From: Radek Pazdera <rpazdera(a)redhat.com>
Code that handles parsing aliases was refactored and moved from
NetTestParse into a new class in Common module - XmlPreprocessor.
In the process of this transition it was extended with a new type
of template - functions.
Template functions can be used the same way as aliases - inside of
curly braces. For instance:
{ip(1,2,3)}
{function(argument1, $alias_argument)}
There are three template functions available at the moment. All three
can be used for retrieving information from machine configs:
hwaddr(machine_id, if_id) - get hw address of an interface
devname(machine_id, if_id) - get interface name
ip(machine_id, if_id, addr_id) - get IP address (if there's a mask
present in the configuration it
will be stripped). The third
argument is optional, defaults to
zero if omitted.
Aliases can be passed to functions as parameters, but the preprocessor
does not support nesting (e.g. function cannot have another function
passed as an argument).
Signed-off-by: Radek Pazdera <rpazdera(a)redhat.com>
---
Common/XmlPreprocessor.py | 226 +++++++++++++++++++++++++++++++++++++++++++++
NetTest/NetTestParse.py | 69 +++-----------
2 files changed, 238 insertions(+), 57 deletions(-)
create mode 100644 Common/XmlPreprocessor.py
diff --git a/Common/XmlPreprocessor.py b/Common/XmlPreprocessor.py
new file mode 100644
index 0000000..88ae758
--- /dev/null
+++ b/Common/XmlPreprocessor.py
@@ -0,0 +1,226 @@
+"""
+This module contains code for preprocessing templates in XML files/recipes
+before they are parsed.
+
+Templates are strings enclosed in curly braces {} and can be present
+in all text elements of the XML file (this includes tag values or
+attribute values). Templates cannot be used as a stubstitution for tag
+names, attribute names or any other structural elements of the document.
+
+There are two supported types of templates:
+
+ * aliases - $alias_name
+ * functions - function_name(param1, param2)
+
+Copyright 2012 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+rpazdera(a)redhat.com (Radek Pazdera)
+"""
+
+import re
+
+class XmlTemplateError(Exception):
+ pass
+
+class XmlPreprocessor:
+ """
+ This class serves as template processor within a XML DOM tree object.
+ """
+
+ _template_re = "\{([^\{\}]+)\}"
+ _alias_re = "^\$([a-zA-Z0-9_]+)(\[.+\])*$"
+ _func_re = "^([a-zA-Z0-9_]+)\(([^\(\)]*)\)$"
+
+ def __init__(self):
+ self._definitions = {}
+ self._reserved_aliases = ["recipe"]
+
+ def define_alias(self, name, value, skip_reserved_check=False):
+ """
+ Associate an alias name with some value. The value can be of
+ an atomic type or an array.
+ """
+
+ if not name in self._reserved_aliases \
+ or skip_reserved_check == True:
+ self._definitions[name] = value
+ else:
+ raise XmlTemplateError("Alias name '%s' is reserved" %
name)
+
+ def expand(self, node):
+ """
+ Traverse DOM tree from `node' down and expand any
+ templates along the way.
+ """
+
+ if node.nodeType == node.ELEMENT_NODE:
+ i = 0
+ num_attributes = node.attributes.length
+ while(i < num_attributes):
+ attr = node.attributes.item(i)
+ attr.value = self._expand_string(str(attr.value))
+ i += 1
+ elif node.nodeType == node.TEXT_NODE:
+ node.data = self._expand_string(str(node.data))
+
+ for child in node.childNodes:
+ self.expand(child)
+
+ def expand_group(self, group):
+ """
+ Behaves exactly the same as the `expand' method, but it
+ operates on a group of DOM nodes stored within a list,
+ rather than a single node.
+ """
+
+ for node in group:
+ self.expand(node)
+
+ def _expand_string(self, string):
+ while True:
+ template_match = re.search(self._template_re, string)
+ if template_match:
+ template_string = template_match.group(0)
+ template = template_match.group(1)
+ template_result = self._process_template(template)
+
+ string = string.replace(template_string, template_result)
+ else:
+ break
+
+ return string
+
+ def _process_template(self, string):
+ string = string.strip()
+ result = None
+
+ if re.match(self._alias_re, string):
+ result = self._process_alias_template(string)
+ return result
+
+ if re.match(self._func_re, string):
+ result = self._process_func_template(string)
+ return result
+
+ raise XmlTemplateError("Unknown template type '%s'" % string)
+
+ def _process_alias_template(self, string):
+ result = None
+
+ alias_match = re.match(self._alias_re, string)
+ if alias_match:
+ alias_name = alias_match.group(1)
+ array_subscript = alias_match.group(2)
+
+ if not alias_name in self._definitions:
+ err = "Alias '%s' is not defined here" % alias_name
+ raise XmlTemplateError(err)
+
+ if array_subscript != None:
+ try:
+ result = str(eval("self._definitions['%s']%s" \
+ % (alias_name, array_subscript)))
+ except (KeyError, IndexError):
+ err = "Wrong array subscript in '%s[%s]'" \
+ % (alias_name, array_subscript)
+ raise XmlTemplateError(err)
+ else:
+ result = self._definitions[alias_name]
+
+ return result
+
+ def _process_func_template(self, string):
+ result = None
+
+ func_match = re.match(self._func_re, string)
+ if func_match:
+ func_name = func_match.group(1)
+ func_params = func_match.group(2)
+
+ if func_params == None:
+ func_params = []
+ else:
+ func_params = func_params.split(",")
+
+ param_values = []
+ for param in func_params:
+ param = param.strip()
+ if re.match(self._alias_re, param):
+ param = self._process_alias_template(param)
+ param_values.append(param)
+
+ result = self._call_preprocessor_func(func_name, param_values)
+
+ return result
+
+ def _call_preprocessor_func(self, name, params):
+ if name == "ip":
+ result = self._ip_func(params)
+ elif name == "hwaddr":
+ result = self._hwaddr_func(params)
+ elif name == "devname":
+ result = self._devname_func(params)
+ else:
+ raise XmlTemplateError("Unknown preprocessor function '%s'"
% name)
+
+ return result
+
+ def _ip_func(self, params):
+ self._validate_func_params("ip", params, 2, 1)
+ self._check_recipe_data("ip")
+
+ m_id = int(params[0])
+ if_id = int(params[1])
+ ip_id = int(params[2]) if len(params) == 3 else 0
+
+ machines = self._definitions["recipe"]["machines"]
+ ip_addr = machines[m_id]['netconfig'][if_id]['addresses'][ip_id]
+
+ return ip_addr.split('/')[0]
+
+ def _hwaddr_func(self, params):
+ self._validate_func_params("hwaddr", params, 2, 0)
+ self._check_recipe_data("hwaddr")
+ m_id = int(params[0])
+ if_id = int(params[1])
+
+ machines = self._definitions["recipe"]["machines"]
+ return machines[m_id]['netconfig'][if_id]['hwaddr']
+
+
+ def _devname_func(self, params):
+ self._validate_func_params("devname", params, 2, 0)
+ self._check_recipe_data("devname")
+ m_id = int(params[0])
+ if_id = int(params[1])
+
+ machines = self._definitions["recipe"]["machines"]
+ return machines[m_id]['netconfig'][if_id]['name']
+
+ @staticmethod
+ def _validate_func_params(name, params, mandatory, optional):
+ num_params = len(params)
+ if num_params > (mandatory + optional) or num_params < mandatory:
+ if optional:
+ err = "Function %s takes between %d-%d arguments, %d passed" \
+ % (name, mandatory, mandatory + optional, num_params)
+ else:
+ err = "Function %s takes %d arguments, %d passed" \
+ % (name, mandatory, num_params)
+ raise XmlTemplateError(err)
+ for param in params:
+ try:
+ int(param)
+ except ValueError:
+ err = "Non-integer parameter passed to '%s'" % name
+ raise XmlTemplateError(err)
+
+ def _check_recipe_data(self, template_name):
+ if not "recipe" in self._definitions:
+ err = "Cannot resolve %s() here, recipe data not available yet" \
+ % template_name
+ raise XmlTemplateError(err)
diff --git a/NetTest/NetTestParse.py b/NetTest/NetTestParse.py
index 69833b1..5b5101a 100644
--- a/NetTest/NetTestParse.py
+++ b/NetTest/NetTestParse.py
@@ -16,6 +16,7 @@ import os
import re
from NetConfig.NetConfigParse import NetConfigParse
from NetTest.NetTestCommand import str_command
+from Common.XmlPreprocessor import XmlPreprocessor
def load_file(filename):
handle = open(filename, "r")
@@ -36,6 +37,8 @@ class NetTestParse:
self._dirpath = os.path.dirname(recipe_path)
self._recipe = None
+ self._xml_prep = XmlPreprocessor()
+
def _get_referenced_xml_path(self, filename):
return os.path.join(self._dirpath, os.path.expanduser(filename))
@@ -64,38 +67,40 @@ class NetTestParse:
return machines
def _parse_definitions(self, dom_definitions_grp):
+ xml_prep = self._xml_prep
definitions = {}
for dom_definitions_item in dom_definitions_grp:
dom_aliases = dom_definitions_item.getElementsByTagName("alias")
for dom_alias in dom_aliases:
alias_name = str(dom_alias.getAttribute("name"))
alias_value = str(dom_alias.getAttribute("value"))
- definitions[alias_name] = alias_value
- return definitions
+ xml_prep.define_alias(alias_name, alias_value)
def parse_recipe(self):
recipe = {}
dom = parseString(self._recipe_xml_string)
+ xml_prep = self._xml_prep
self._load_included_parts(dom)
dom_nettestrecipe = dom.getElementsByTagName("nettestrecipe")[0]
dom_definitions_grp = dom_nettestrecipe.getElementsByTagName("define")
- self._definitions = self._parse_definitions(dom_definitions_grp)
+ self._parse_definitions(dom_definitions_grp)
for define_tag in dom_definitions_grp:
parent = define_tag.parentNode
parent.removeChild(define_tag)
dom_machines_grp = dom_nettestrecipe.getElementsByTagName("machines")
- self._expand_group(dom_machines_grp)
+ xml_prep.expand_group(dom_machines_grp)
recipe["machines"] = self._parse_machines(dom_machines_grp)
dom_switches_grp = dom_nettestrecipe.getElementsByTagName("switches")
- self._expand_group(dom_switches_grp)
+ xml_prep.expand_group(dom_switches_grp)
recipe["switches"] = self._parse_machines(dom_switches_grp)
self._recipe = recipe
self._dom_nettestrecipe = dom_nettestrecipe
+ xml_prep.define_alias("recipe", recipe, skip_reserved_check=True)
def get_recipe(self):
return self._recipe
@@ -261,7 +266,8 @@ class NetTestParse:
def parse_recipe_command_sequence(self):
dom_sequences =
self._dom_nettestrecipe.getElementsByTagName("command_sequence")
- self._expand_group(dom_sequences, recipe_eval=True)
+ xml_prep = self._xml_prep
+ xml_prep.expand_group(dom_sequences)
self._recipe["sequences"] = []
for dom_sequence in dom_sequences:
@@ -272,54 +278,3 @@ class NetTestParse:
self._check_sequence(sequence)
self._recipe["sequences"].append(sequence)
-
- def _expand(self, node, recipe_eval=False):
- if node.nodeType == node.ELEMENT_NODE:
- i = 0
- num_attributes = node.attributes.length
- while(i < num_attributes):
- attr = node.attributes.item(i)
- attr.value = self._expand_string(str(attr.value), recipe_eval)
- i += 1
- elif node.nodeType == node.TEXT_NODE:
- node.data = self._expand_string(str(node.data), recipe_eval)
-
- for child in node.childNodes:
- self._expand(child, recipe_eval)
-
- def _expand_group(self, group, recipe_eval=False):
- for node in group:
- self._expand(node, recipe_eval)
-
- def _expand_string(self, string, recipe_eval):
- eval_re = "\{\$recipe([^\{\}]+)\}"
- alias_re = "\{\$([^\{\}]*)\}"
- while True:
- eval_match = re.search(eval_re, string)
- if eval_match:
- eval_string = eval_match.group(0)
- eval_data = eval_match.group(1)
- if recipe_eval:
- string = string.replace(eval_string,
- self._recipe_eval(eval_data))
- continue
- else:
- err = ("Accessing $recipe allowed only from command sequence:
%s"
- % string)
- raise KeyError(err)
- alias_match = re.search(alias_re, string)
- if alias_match:
- alias = alias_match.group(0)
- alias_name = alias_match.group(1)
- try:
- string = string.replace(alias,
- self._definitions[alias_name])
- continue
- except KeyError, err:
- raise Exception("Alias '%s' doesn't exist!" %
str(err))
-
-
- if not (eval_match and alias_match):
- break
-
- return string
--
1.7.7.6