Bug 240 - NS Scaling
[osm/SO.git] / rwlaunchpad / plugins / rwnsm / rift / tasklets / rwnsmtasklet / xpath.py
1
2 #
3 # Copyright 2016 RIFT.IO Inc
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 #
17
18 import collections
19 import re
20
21
22 class Attribute(collections.namedtuple("Attribute", "module name")):
23 def __repr__(self):
24 return "{}:{}".format(self.module, self.name)
25
26
27 class ListElement(collections.namedtuple("List", "module name key value")):
28 def __repr__(self):
29 return "{}:{}[{}={}]".format(self.module, self.name, self.key, self.value)
30
31
32 def tokenize(xpath):
33 """Return a list of tokens representing an xpath
34
35 The types of xpaths that this selector supports is extremely limited.
36 The xpath is required to be an absolute path delimited by a
37 forward-slash. Each of the parts (elements between delimiters) is
38 treated as one of two possible types:
39
40 - an attribute
41 - a list element
42
43 An attribute is a normal python attribute on an object. A list element
44 is an element within a list, which is identified by a key value (like a
45 yang list, although this is more properly a dict in python).
46
47 Each attribute is expected to have the form,
48
49 <namespace>:<variable-name>
50
51 A valid variable name (or namespace) follows the python regular expression,
52
53 [a-zA-Z0-9-_]+
54
55 A list entry has the form,
56
57 <namespace>:<variable-name>[<namespace>:<variable-name>=<value>]
58
59 The expression in the square brackets is the key of the required
60 element, and the value that that key must have.
61
62 Arguments:
63 xpath - a string containing an xpath expression
64
65 Raises:
66 A ValueError is raised if the xpath cannot be parsed.
67
68 Returns:
69 a list of tokens
70
71 """
72 # define the symbols that are valid for a variable name in yang
73 name = "[a-zA-Z0-9-_]+"
74
75 # define a set of regular expressions for parsing the xpath
76 pattern_attribute = re.compile("({t}):({t})$".format(t=name))
77 pattern_key_value = re.compile("^{t}:({t})\s*=\s*(.*)$".format(t=name))
78 pattern_quote = re.compile("^[\'\"](.*)[\'\"]$")
79 pattern_list = re.compile("^(.*)\[(.*)\]$")
80
81 def dash_to_underscore(text):
82 return text.replace('-', '_')
83
84 # Iterate through the parts of the xpath (NB: because the xpaths are
85 # required to be absolute paths, the first character is going to be the
86 # forward slash. As a result, when the string is split, the first
87 # element with be an empty string).
88 tokens = list()
89 for part in xpath.split("/")[1:]:
90
91 # Test the part to see if it is a attribute
92 result = pattern_attribute.match(part)
93 if result is not None:
94 module, name = result.groups()
95
96 # Convert the dashes to underscores
97 name = dash_to_underscore(name)
98 module = dash_to_underscore(module)
99
100 tokens.append(Attribute(module, name))
101
102 continue
103
104 # Test the part to see if it is a list
105 result = pattern_list.match(part)
106 if result is not None:
107 attribute, keyvalue = result.groups()
108
109 module, name = pattern_attribute.match(attribute).groups()
110 key, value = pattern_key_value.match(keyvalue).groups()
111
112 # Convert the dashes to underscore (but not in the key value)
113 key = dash_to_underscore(key)
114 name = dash_to_underscore(name)
115 module = dash_to_underscore(module)
116
117 result = pattern_quote.match(value)
118 if result is not None:
119 value = result.group(1)
120
121 tokens.append(ListElement(module, name, key, value))
122
123 continue
124
125 raise ValueError("cannot parse '{}'".format(part))
126
127 return tokens
128
129
130 class XPathAttribute(object):
131 """
132 This class is used to represent a reference to an attribute. If you use
133 getattr on an attribute, it may give you the value of the attribute rather
134 than a reference to it. What is really wanted is a representation of the
135 attribute so that its value can be both retrieved and set. That is what
136 this class provides.
137 """
138
139 def __init__(self, obj, name):
140 """Create an instance of XPathAttribute
141
142 Arguments:
143 obj - the object containing the attribute
144 name - the name of an attribute
145
146 Raises:
147 A ValueError is raised if the provided object does not have the
148 associated attribute.
149
150 """
151 if not hasattr(obj, name):
152 msg = "The provided object does not contain the associated attribute"
153 raise ValueError(msg)
154
155 self.obj = obj
156 self.name = name
157
158 def __repr__(self):
159 return self.value
160
161 @property
162 def value(self):
163 return getattr(self.obj, self.name)
164
165 @value.setter
166 def value(self, value):
167 """Set the value of the attribute
168
169 Arguments:
170 value - the new value that the attribute should take
171
172 Raises:
173 An TypeError is raised if the provided value cannot be cast the
174 current type of the attribute.
175
176 """
177 attr_type = type(self.value)
178 attr_value = value
179
180 # The only way we can currently get the type of the atrribute is if it
181 # has an existing value. So if the attribute has an existing value,
182 # cast the value to the type of the attribute value.
183 if attr_type is not type(None):
184 try:
185 attr_value = attr_type(attr_value)
186
187 except ValueError:
188 msg = "expected type '{}', but got '{}' instead"
189 raise TypeError(msg.format(attr_type.__name__, type(value).__name__))
190
191 setattr(self.obj, self.name, attr_value)
192
193
194 class XPathElement(XPathAttribute):
195 """
196 This class is used to represent a reference to an element within a list.
197 Unlike scalar attributes, it is not entirely necessary to have this class
198 to represent the attribute because the element cannot be a simple scalar.
199 However, this class is used because it creates a uniform interface that can
200 be used by the setxattr and getxattr functions.
201 """
202
203 def __init__(self, container, key, value):
204 """Create an instance of XPathElement
205
206 Arguments:
207 container - the object that contains the element
208 key - the name of the field that is used to identify the
209 element
210 value - the value of the key that identifies the element
211
212 """
213 self._container = container
214 self._value = value
215 self._key = key
216
217 @property
218 def value(self):
219 for element in self._container:
220 if getattr(element, self._key) == self._value:
221 return element
222
223 raise ValueError("specified element does not exist")
224
225 @value.setter
226 def value(self, value):
227 existing = None
228 for element in self._container:
229 if getattr(element, self._key) == self._value:
230 existing = element
231 break
232
233 if existing is not None:
234 self._container.remove(existing)
235
236 self._container.append(value)
237
238
239 class XPathSelector(object):
240 def __init__(self, xpath):
241 """Creates an instance of XPathSelector
242
243 Arguments:
244 xpath - a string containing an xpath expression
245
246 """
247 self._tokens = tokenize(xpath)
248
249
250 def __call__(self, obj):
251 """Returns a reference to an attribute on the provided object
252
253 Using the defined xpath, an attribute is selected from the provided
254 object and returned.
255
256 Arguments:
257 obj - a GI object
258
259 Raises:
260 A ValueError is raised if the specified element in a list cannot be
261 found.
262
263 Returns:
264 an XPathAttribute that reference the specified attribute
265
266 """
267 current = obj
268 for token in self._tokens[:-1]:
269 # If the object is contained within a list, we will need to iterate
270 # through the tokens until we find a token that is a field of the
271 # object.
272 if token.name not in current.fields:
273 if current is obj:
274 continue
275
276 raise ValueError('cannot find attribute {}'.format(token.name))
277
278 # If the token is a ListElement, try to find the matching element
279 if isinstance(token, ListElement):
280 for element in getattr(current, token.name):
281 if getattr(element, token.key) == token.value:
282 current = element
283 break
284
285 else:
286 raise ValueError('unable to find {}'.format(token.value))
287
288 else:
289 # Attribute the variable matching the name of the token
290 current = getattr(current, token.name)
291
292 # Process the final token
293 token = self._tokens[-1]
294
295 # If the token represents a list element, find the element in the list
296 # and return an XPathElement
297 if isinstance(token, ListElement):
298 container = getattr(current, token.name)
299 for element in container:
300 if getattr(element, token.key) == token.value:
301 return XPathElement(container, token.key, token.value)
302
303 else:
304 raise ValueError('unable to find {}'.format(token.value))
305
306 # Otherwise, return the object as an XPathAttribute
307 return XPathAttribute(current, token.name)
308
309 @property
310 def tokens(self):
311 """The tokens in the xpath expression"""
312 return self._tokens
313
314
315 # A global cache to avoid repeated parsing of known xpath expressions
316 __xpath_cache = dict()
317
318
319 def reset_cache():
320 global __xpath_cache
321 __xpath_cache = dict()
322
323
324 def getxattr(obj, xpath):
325 """Return an attribute on the provided object
326
327 The xpath is parsed and used to identify an attribute on the provided
328 object. The object is expected to be a GI object where each attribute that
329 is accessible via an xpath expression is contained in the 'fields'
330 attribute of the object (NB: this is not true of GI lists, which do not
331 have a 'fields' attribute).
332
333 A selector is create for each xpath and used to find the specified
334 attribute. The accepted xpath expressions are those supported by the
335 XPathSelector class. The parsed xpath expression is cached so that
336 subsequent parsing is unnecessary. However, selectors are stored in a
337 global dictionary and this means that this function is not thread-safe.
338
339 Arguments:
340 obj - a GI object
341 xpath - a string containing an xpath expression
342
343 Returns:
344 an attribute on the provided object
345
346 """
347 if xpath not in __xpath_cache:
348 __xpath_cache[xpath] = XPathSelector(xpath)
349
350 return __xpath_cache[xpath](obj).value
351
352
353 def setxattr(obj, xpath, value):
354 """Set the attribute referred to by the xpath
355
356 Arguments:
357 obj - a GI object
358 xpath - a string containing an xpath expression
359 value - the new value of the attribute
360
361 """
362 if xpath not in __xpath_cache:
363 __xpath_cache[xpath] = XPathSelector(xpath)
364
365 __xpath_cache[xpath](obj).value = value