Adding new multi-allocation request
[ganeti-local] / lib / ht.py
1 #
2 #
3
4 # Copyright (C) 2010, 2011 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Module implementing the parameter types code."""
23
24 import re
25 import operator
26
27 from ganeti import compat
28 from ganeti import utils
29 from ganeti import constants
30
31
32 _PAREN_RE = re.compile("^[a-zA-Z0-9_-]+$")
33
34
35 def Parens(text):
36   """Enclose text in parens if necessary.
37
38   @param text: Text
39
40   """
41   text = str(text)
42
43   if _PAREN_RE.match(text):
44     return text
45   else:
46     return "(%s)" % text
47
48
49 class _WrapperBase(object):
50   __slots__ = [
51     "_fn",
52     "_text",
53     ]
54
55   def __init__(self, text, fn):
56     """Initializes this class.
57
58     @param text: Description
59     @param fn: Wrapped function
60
61     """
62     assert text.strip()
63
64     self._text = text
65     self._fn = fn
66
67   def __call__(self, *args):
68     return self._fn(*args)
69
70
71 class _DescWrapper(_WrapperBase):
72   """Wrapper class for description text.
73
74   """
75   def __str__(self):
76     return self._text
77
78
79 class _CommentWrapper(_WrapperBase):
80   """Wrapper class for comment.
81
82   """
83   def __str__(self):
84     return "%s [%s]" % (self._fn, self._text)
85
86
87 def WithDesc(text):
88   """Builds wrapper class with description text.
89
90   @type text: string
91   @param text: Description text
92   @return: Callable class
93
94   """
95   assert text[0] == text[0].upper()
96
97   return compat.partial(_DescWrapper, text)
98
99
100 def Comment(text):
101   """Builds wrapper for adding comment to description text.
102
103   @type text: string
104   @param text: Comment text
105   @return: Callable class
106
107   """
108   assert not frozenset(text).intersection("[]")
109
110   return compat.partial(_CommentWrapper, text)
111
112
113 def CombinationDesc(op, args, fn):
114   """Build description for combinating operator.
115
116   @type op: string
117   @param op: Operator as text (e.g. "and")
118   @type args: list
119   @param args: Operator arguments
120   @type fn: callable
121   @param fn: Wrapped function
122
123   """
124   if len(args) == 1:
125     descr = str(args[0])
126   else:
127     descr = (" %s " % op).join(Parens(i) for i in args)
128
129   return WithDesc(descr)(fn)
130
131
132 # Modifiable default values; need to define these here before the
133 # actual LUs
134
135 @WithDesc(str([]))
136 def EmptyList():
137   """Returns an empty list.
138
139   """
140   return []
141
142
143 @WithDesc(str({}))
144 def EmptyDict():
145   """Returns an empty dict.
146
147   """
148   return {}
149
150
151 #: The without-default default value
152 NoDefault = object()
153
154
155 #: The no-type (value too complex to check it in the type system)
156 NoType = object()
157
158
159 # Some basic types
160 @WithDesc("Anything")
161 def TAny(_):
162   """Accepts any value.
163
164   """
165   return True
166
167
168 @WithDesc("NotNone")
169 def TNotNone(val):
170   """Checks if the given value is not None.
171
172   """
173   return val is not None
174
175
176 @WithDesc("None")
177 def TNone(val):
178   """Checks if the given value is None.
179
180   """
181   return val is None
182
183
184 @WithDesc("Boolean")
185 def TBool(val):
186   """Checks if the given value is a boolean.
187
188   """
189   return isinstance(val, bool)
190
191
192 @WithDesc("Integer")
193 def TInt(val):
194   """Checks if the given value is an integer.
195
196   """
197   # For backwards compatibility with older Python versions, boolean values are
198   # also integers and should be excluded in this test.
199   #
200   # >>> (isinstance(False, int), isinstance(True, int))
201   # (True, True)
202   return isinstance(val, (int, long)) and not isinstance(val, bool)
203
204
205 @WithDesc("Float")
206 def TFloat(val):
207   """Checks if the given value is a float.
208
209   """
210   return isinstance(val, float)
211
212
213 @WithDesc("String")
214 def TString(val):
215   """Checks if the given value is a string.
216
217   """
218   return isinstance(val, basestring)
219
220
221 @WithDesc("EvalToTrue")
222 def TTrue(val):
223   """Checks if a given value evaluates to a boolean True value.
224
225   """
226   return bool(val)
227
228
229 def TElemOf(target_list):
230   """Builds a function that checks if a given value is a member of a list.
231
232   """
233   def fn(val):
234     return val in target_list
235
236   return WithDesc("OneOf %s" % (utils.CommaJoin(target_list), ))(fn)
237
238
239 # Container types
240 @WithDesc("List")
241 def TList(val):
242   """Checks if the given value is a list.
243
244   """
245   return isinstance(val, list)
246
247
248 @WithDesc("Dictionary")
249 def TDict(val):
250   """Checks if the given value is a dictionary.
251
252   """
253   return isinstance(val, dict)
254
255
256 def TIsLength(size):
257   """Check is the given container is of the given size.
258
259   """
260   def fn(container):
261     return len(container) == size
262
263   return WithDesc("Length %s" % (size, ))(fn)
264
265
266 # Combinator types
267 def TAnd(*args):
268   """Combine multiple functions using an AND operation.
269
270   """
271   def fn(val):
272     return compat.all(t(val) for t in args)
273
274   return CombinationDesc("and", args, fn)
275
276
277 def TOr(*args):
278   """Combine multiple functions using an AND operation.
279
280   """
281   def fn(val):
282     return compat.any(t(val) for t in args)
283
284   return CombinationDesc("or", args, fn)
285
286
287 def TMap(fn, test):
288   """Checks that a modified version of the argument passes the given test.
289
290   """
291   return WithDesc("Result of %s must be %s" %
292                   (Parens(fn), Parens(test)))(lambda val: test(fn(val)))
293
294
295 def TRegex(pobj):
296   """Checks whether a string matches a specific regular expression.
297
298   @param pobj: Compiled regular expression as returned by C{re.compile}
299
300   """
301   desc = WithDesc("String matching regex \"%s\"" %
302                   pobj.pattern.encode("string_escape"))
303
304   return desc(TAnd(TString, pobj.match))
305
306
307 # Type aliases
308
309 #: a non-empty string
310 TNonEmptyString = WithDesc("NonEmptyString")(TAnd(TString, TTrue))
311
312 #: a maybe non-empty string
313 TMaybeString = TOr(TNonEmptyString, TNone)
314
315 #: a maybe boolean (bool or none)
316 TMaybeBool = TOr(TBool, TNone)
317
318 #: Maybe a dictionary (dict or None)
319 TMaybeDict = TOr(TDict, TNone)
320
321 #: a positive integer
322 TPositiveInt = \
323   TAnd(TInt, WithDesc("EqualGreaterZero")(lambda v: v >= 0))
324
325 #: a maybe positive integer (positive integer or None)
326 TMaybePositiveInt = TOr(TPositiveInt, TNone)
327
328 #: a strictly positive integer
329 TStrictPositiveInt = \
330   TAnd(TInt, WithDesc("GreaterThanZero")(lambda v: v > 0))
331
332 #: a maybe strictly positive integer (strictly positive integer or None)
333 TMaybeStrictPositiveInt = TOr(TStrictPositiveInt, TNone)
334
335 #: a strictly negative integer (0 > value)
336 TStrictNegativeInt = \
337   TAnd(TInt, WithDesc("LessThanZero")(compat.partial(operator.gt, 0)))
338
339 #: a positive float
340 TPositiveFloat = \
341   TAnd(TFloat, WithDesc("EqualGreaterZero")(lambda v: v >= 0.0))
342
343 #: Job ID
344 TJobId = WithDesc("JobId")(TOr(TPositiveInt,
345                                TRegex(re.compile("^%s$" %
346                                                  constants.JOB_ID_TEMPLATE))))
347
348 #: Number
349 TNumber = TOr(TInt, TFloat)
350
351 #: Relative job ID
352 TRelativeJobId = WithDesc("RelativeJobId")(TStrictNegativeInt)
353
354
355 def TInstanceOf(my_inst):
356   """Checks if a given value is an instance of my_inst.
357
358   """
359   desc = WithDesc("Instance of %s" % (Parens(my_inst), ))
360   return desc(lambda val: isinstance(val, my_inst))
361
362
363 def TListOf(my_type):
364   """Checks if a given value is a list with all elements of the same type.
365
366   """
367   desc = WithDesc("List of %s" % (Parens(my_type), ))
368   return desc(TAnd(TList, lambda lst: compat.all(my_type(v) for v in lst)))
369
370
371 TMaybeListOf = lambda item_type: TOr(TNone, TListOf(item_type))
372
373
374 def TDictOf(key_type, val_type):
375   """Checks a dict type for the type of its key/values.
376
377   """
378   desc = WithDesc("Dictionary with keys of %s and values of %s" %
379                   (Parens(key_type), Parens(val_type)))
380
381   def fn(container):
382     return (compat.all(key_type(v) for v in container.keys()) and
383             compat.all(val_type(v) for v in container.values()))
384
385   return desc(TAnd(TDict, fn))
386
387
388 def _TStrictDictCheck(require_all, exclusive, items, val):
389   """Helper function for L{TStrictDict}.
390
391   """
392   notfound_fn = lambda _: not exclusive
393
394   if require_all and not frozenset(val.keys()).issuperset(items.keys()):
395     # Requires items not found in value
396     return False
397
398   return compat.all(items.get(key, notfound_fn)(value)
399                     for (key, value) in val.items())
400
401
402 def TStrictDict(require_all, exclusive, items):
403   """Strict dictionary check with specific keys.
404
405   @type require_all: boolean
406   @param require_all: Whether all keys in L{items} are required
407   @type exclusive: boolean
408   @param exclusive: Whether only keys listed in L{items} should be accepted
409   @type items: dictionary
410   @param items: Mapping from key (string) to verification function
411
412   """
413   descparts = ["Dictionary containing"]
414
415   if exclusive:
416     descparts.append(" none but the")
417
418   if require_all:
419     descparts.append(" required")
420
421   if len(items) == 1:
422     descparts.append(" key ")
423   else:
424     descparts.append(" keys ")
425
426   descparts.append(utils.CommaJoin("\"%s\" (value %s)" % (key, value)
427                                    for (key, value) in items.items()))
428
429   desc = WithDesc("".join(descparts))
430
431   return desc(TAnd(TDict,
432                    compat.partial(_TStrictDictCheck, require_all, exclusive,
433                                   items)))
434
435
436 def TItems(items):
437   """Checks individual items of a container.
438
439   If the verified value and the list of expected items differ in length, this
440   check considers only as many items as are contained in the shorter list. Use
441   L{TIsLength} to enforce a certain length.
442
443   @type items: list
444   @param items: List of checks
445
446   """
447   assert items, "Need items"
448
449   text = ["Item", "item"]
450   desc = WithDesc(utils.CommaJoin("%s %s is %s" %
451                                   (text[int(idx > 0)], idx, Parens(check))
452                                   for (idx, check) in enumerate(items)))
453
454   return desc(lambda value: compat.all(check(i)
455                                        for (check, i) in zip(items, value)))