Merge 'EvacNode' and 'NodeEvacMode'
[ganeti-local] / lib / ht.py
1 #
2 #
3
4 # Copyright (C) 2010, 2011, 2012 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 import ipaddr
27
28 from ganeti import compat
29 from ganeti import utils
30 from ganeti import constants
31 from ganeti import objects
32
33
34 _PAREN_RE = re.compile("^[a-zA-Z0-9_-]+$")
35
36
37 def Parens(text):
38   """Enclose text in parens if necessary.
39
40   @param text: Text
41
42   """
43   text = str(text)
44
45   if _PAREN_RE.match(text):
46     return text
47   else:
48     return "(%s)" % text
49
50
51 class _WrapperBase(object):
52   __slots__ = [
53     "_fn",
54     "_text",
55     ]
56
57   def __init__(self, text, fn):
58     """Initializes this class.
59
60     @param text: Description
61     @param fn: Wrapped function
62
63     """
64     assert text.strip()
65
66     self._text = text
67     self._fn = fn
68
69   def __call__(self, *args):
70     return self._fn(*args)
71
72
73 class _DescWrapper(_WrapperBase):
74   """Wrapper class for description text.
75
76   """
77   def __str__(self):
78     return self._text
79
80
81 class _CommentWrapper(_WrapperBase):
82   """Wrapper class for comment.
83
84   """
85   def __str__(self):
86     return "%s [%s]" % (self._fn, self._text)
87
88
89 def WithDesc(text):
90   """Builds wrapper class with description text.
91
92   @type text: string
93   @param text: Description text
94   @return: Callable class
95
96   """
97   assert text[0] == text[0].upper()
98
99   return compat.partial(_DescWrapper, text)
100
101
102 def Comment(text):
103   """Builds wrapper for adding comment to description text.
104
105   @type text: string
106   @param text: Comment text
107   @return: Callable class
108
109   """
110   assert not frozenset(text).intersection("[]")
111
112   return compat.partial(_CommentWrapper, text)
113
114
115 def CombinationDesc(op, args, fn):
116   """Build description for combinating operator.
117
118   @type op: string
119   @param op: Operator as text (e.g. "and")
120   @type args: list
121   @param args: Operator arguments
122   @type fn: callable
123   @param fn: Wrapped function
124
125   """
126   # Some type descriptions are rather long. If "None" is listed at the
127   # end or somewhere in between it is easily missed. Therefore it should
128   # be at the beginning, e.g. "None or (long description)".
129   if __debug__ and TNone in args and args.index(TNone) > 0:
130     raise Exception("TNone must be listed first")
131
132   if len(args) == 1:
133     descr = str(args[0])
134   else:
135     descr = (" %s " % op).join(Parens(i) for i in args)
136
137   return WithDesc(descr)(fn)
138
139
140 # Modifiable default values; need to define these here before the
141 # actual LUs
142
143 @WithDesc(str([]))
144 def EmptyList():
145   """Returns an empty list.
146
147   """
148   return []
149
150
151 @WithDesc(str({}))
152 def EmptyDict():
153   """Returns an empty dict.
154
155   """
156   return {}
157
158
159 #: The without-default default value
160 NoDefault = object()
161
162
163 # Some basic types
164 @WithDesc("Anything")
165 def TAny(_):
166   """Accepts any value.
167
168   """
169   return True
170
171
172 @WithDesc("NotNone")
173 def TNotNone(val):
174   """Checks if the given value is not None.
175
176   """
177   return val is not None
178
179
180 @WithDesc("None")
181 def TNone(val):
182   """Checks if the given value is None.
183
184   """
185   return val is None
186
187
188 @WithDesc("ValueNone")
189 def TValueNone(val):
190   """Checks if the given value is L{constants.VALUE_NONE}.
191
192   """
193   return val == constants.VALUE_NONE
194
195
196 @WithDesc("Boolean")
197 def TBool(val):
198   """Checks if the given value is a boolean.
199
200   """
201   return isinstance(val, bool)
202
203
204 @WithDesc("Integer")
205 def TInt(val):
206   """Checks if the given value is an integer.
207
208   """
209   # For backwards compatibility with older Python versions, boolean values are
210   # also integers and should be excluded in this test.
211   #
212   # >>> (isinstance(False, int), isinstance(True, int))
213   # (True, True)
214   return isinstance(val, (int, long)) and not isinstance(val, bool)
215
216
217 @WithDesc("Float")
218 def TFloat(val):
219   """Checks if the given value is a float.
220
221   """
222   return isinstance(val, float)
223
224
225 @WithDesc("String")
226 def TString(val):
227   """Checks if the given value is a string.
228
229   """
230   return isinstance(val, basestring)
231
232
233 @WithDesc("EvalToTrue")
234 def TTrue(val):
235   """Checks if a given value evaluates to a boolean True value.
236
237   """
238   return bool(val)
239
240
241 def TElemOf(target_list):
242   """Builds a function that checks if a given value is a member of a list.
243
244   """
245   def fn(val):
246     return val in target_list
247
248   return WithDesc("OneOf %s" % (utils.CommaJoin(target_list), ))(fn)
249
250
251 # Container types
252 @WithDesc("List")
253 def TList(val):
254   """Checks if the given value is a list.
255
256   """
257   return isinstance(val, list)
258
259
260 @WithDesc("Tuple")
261 def TTuple(val):
262   """Checks if the given value is a tuple.
263
264   """
265   return isinstance(val, tuple)
266
267
268 @WithDesc("Dictionary")
269 def TDict(val):
270   """Checks if the given value is a dictionary.
271
272   """
273   return isinstance(val, dict)
274
275
276 def TIsLength(size):
277   """Check is the given container is of the given size.
278
279   """
280   def fn(container):
281     return len(container) == size
282
283   return WithDesc("Length %s" % (size, ))(fn)
284
285
286 # Combinator types
287 def TAnd(*args):
288   """Combine multiple functions using an AND operation.
289
290   """
291   def fn(val):
292     return compat.all(t(val) for t in args)
293
294   return CombinationDesc("and", args, fn)
295
296
297 def TOr(*args):
298   """Combine multiple functions using an OR operation.
299
300   """
301   def fn(val):
302     return compat.any(t(val) for t in args)
303
304   return CombinationDesc("or", args, fn)
305
306
307 def TMap(fn, test):
308   """Checks that a modified version of the argument passes the given test.
309
310   """
311   return WithDesc("Result of %s must be %s" %
312                   (Parens(fn), Parens(test)))(lambda val: test(fn(val)))
313
314
315 def TRegex(pobj):
316   """Checks whether a string matches a specific regular expression.
317
318   @param pobj: Compiled regular expression as returned by C{re.compile}
319
320   """
321   desc = WithDesc("String matching regex \"%s\"" %
322                   pobj.pattern.encode("string_escape"))
323
324   return desc(TAnd(TString, pobj.match))
325
326
327 def TMaybe(test):
328   """Wrap a test in a TOr(TNone, test).
329
330   This makes it easier to define TMaybe* types.
331
332   """
333   return TOr(TNone, test)
334
335
336 def TMaybeValueNone(test):
337   """Used for unsetting values.
338
339   """
340   return TMaybe(TOr(TValueNone, test))
341
342
343 # Type aliases
344
345 #: a non-empty string
346 TNonEmptyString = WithDesc("NonEmptyString")(TAnd(TString, TTrue))
347
348 #: a maybe non-empty string
349 TMaybeString = TMaybe(TNonEmptyString)
350
351 #: a maybe boolean (bool or none)
352 TMaybeBool = TMaybe(TBool)
353
354 #: Maybe a dictionary (dict or None)
355 TMaybeDict = TMaybe(TDict)
356
357 #: Maybe a list (list or None)
358 TMaybeList = TMaybe(TList)
359
360
361 #: a non-negative number (value > 0)
362 # val_type should be TInt, TDouble (== TFloat), or TNumber
363 def TNonNegative(val_type):
364   return WithDesc("EqualOrGreaterThanZero")(TAnd(val_type, lambda v: v >= 0))
365
366
367 #: a positive number (value >= 0)
368 # val_type should be TInt, TDouble (== TFloat), or TNumber
369 def TPositive(val_type):
370   return WithDesc("GreaterThanZero")(TAnd(val_type, lambda v: v > 0))
371
372
373 #: a non-negative integer (value >= 0)
374 TNonNegativeInt = TNonNegative(TInt)
375
376 #: a positive integer (value > 0)
377 TPositiveInt = TPositive(TInt)
378
379 #: a maybe positive integer (positive integer or None)
380 TMaybePositiveInt = TMaybe(TPositiveInt)
381
382 #: a negative integer (value < 0)
383 TNegativeInt = \
384   TAnd(TInt, WithDesc("LessThanZero")(compat.partial(operator.gt, 0)))
385
386 #: a positive float
387 TNonNegativeFloat = \
388   TAnd(TFloat, WithDesc("EqualOrGreaterThanZero")(lambda v: v >= 0.0))
389
390 #: Job ID
391 TJobId = WithDesc("JobId")(TOr(TNonNegativeInt,
392                                TRegex(re.compile("^%s$" %
393                                                  constants.JOB_ID_TEMPLATE))))
394
395 #: Double (== Float)
396 TDouble = TFloat
397
398 #: Number
399 TNumber = TOr(TInt, TFloat)
400
401 #: Relative job ID
402 TRelativeJobId = WithDesc("RelativeJobId")(TNegativeInt)
403
404
405 def TInstanceOf(cls):
406   """Checks if a given value is an instance of C{cls}.
407
408   @type cls: class
409   @param cls: Class object
410
411   """
412   name = "%s.%s" % (cls.__module__, cls.__name__)
413
414   desc = WithDesc("Instance of %s" % (Parens(name), ))
415
416   return desc(lambda val: isinstance(val, cls))
417
418
419 def TListOf(my_type):
420   """Checks if a given value is a list with all elements of the same type.
421
422   """
423   desc = WithDesc("List of %s" % (Parens(my_type), ))
424   return desc(TAnd(TList, lambda lst: compat.all(my_type(v) for v in lst)))
425
426
427 TMaybeListOf = lambda item_type: TMaybe(TListOf(item_type))
428
429
430 def TTupleOf(*val_types):
431   """Checks if a given value is a list with the proper size and its
432      elements match the given types.
433
434   """
435   desc = WithDesc("Tuple of %s" % (Parens(val_types), ))
436   return desc(TAnd(TIsLength(len(val_types)), TItems(val_types)))
437
438
439 def TSetOf(val_type):
440   """Checks if a given value is a list with all elements of the same
441      type and eliminates duplicated elements.
442
443   """
444   desc = WithDesc("Set of %s" % (Parens(val_type), ))
445   return desc(lambda st: TListOf(val_type)(list(set(st))))
446
447
448 def TDictOf(key_type, val_type):
449   """Checks a dict type for the type of its key/values.
450
451   """
452   desc = WithDesc("Dictionary with keys of %s and values of %s" %
453                   (Parens(key_type), Parens(val_type)))
454
455   def fn(container):
456     return (compat.all(key_type(v) for v in container.keys()) and
457             compat.all(val_type(v) for v in container.values()))
458
459   return desc(TAnd(TDict, fn))
460
461
462 def _TStrictDictCheck(require_all, exclusive, items, val):
463   """Helper function for L{TStrictDict}.
464
465   """
466   notfound_fn = lambda _: not exclusive
467
468   if require_all and not frozenset(val.keys()).issuperset(items.keys()):
469     # Requires items not found in value
470     return False
471
472   return compat.all(items.get(key, notfound_fn)(value)
473                     for (key, value) in val.items())
474
475
476 def TStrictDict(require_all, exclusive, items):
477   """Strict dictionary check with specific keys.
478
479   @type require_all: boolean
480   @param require_all: Whether all keys in L{items} are required
481   @type exclusive: boolean
482   @param exclusive: Whether only keys listed in L{items} should be accepted
483   @type items: dictionary
484   @param items: Mapping from key (string) to verification function
485
486   """
487   descparts = ["Dictionary containing"]
488
489   if exclusive:
490     descparts.append(" none but the")
491
492   if require_all:
493     descparts.append(" required")
494
495   if len(items) == 1:
496     descparts.append(" key ")
497   else:
498     descparts.append(" keys ")
499
500   descparts.append(utils.CommaJoin("\"%s\" (value %s)" % (key, value)
501                                    for (key, value) in items.items()))
502
503   desc = WithDesc("".join(descparts))
504
505   return desc(TAnd(TDict,
506                    compat.partial(_TStrictDictCheck, require_all, exclusive,
507                                   items)))
508
509
510 def TItems(items):
511   """Checks individual items of a container.
512
513   If the verified value and the list of expected items differ in length, this
514   check considers only as many items as are contained in the shorter list. Use
515   L{TIsLength} to enforce a certain length.
516
517   @type items: list
518   @param items: List of checks
519
520   """
521   assert items, "Need items"
522
523   text = ["Item", "item"]
524   desc = WithDesc(utils.CommaJoin("%s %s is %s" %
525                                   (text[int(idx > 0)], idx, Parens(check))
526                                   for (idx, check) in enumerate(items)))
527
528   return desc(lambda value: compat.all(check(i)
529                                        for (check, i) in zip(items, value)))
530
531
532 TAllocPolicy = TElemOf(constants.VALID_ALLOC_POLICIES)
533 TCVErrorCode = TElemOf(constants.CV_ALL_ECODES_STRINGS)
534 TQueryResultCode = TElemOf(constants.RS_ALL)
535 TExportTarget = TOr(TNonEmptyString, TList)
536 TExportMode = TElemOf(constants.EXPORT_MODES)
537 TDiskIndex = TAnd(TNonNegativeInt, lambda val: val < constants.MAX_DISKS)
538 TReplaceDisksMode = TElemOf(constants.REPLACE_MODES)
539 TDiskTemplate = TElemOf(constants.DISK_TEMPLATES)
540 TEvacMode = TElemOf(constants.NODE_EVAC_MODES)
541 TIAllocatorTestDir = TElemOf(constants.VALID_IALLOCATOR_DIRECTIONS)
542 TIAllocatorMode = TElemOf(constants.VALID_IALLOCATOR_MODES)
543
544
545 def TSetParamsMods(fn):
546   """Generates a check for modification lists.
547
548   """
549   # Old format
550   # TODO: Remove in version 2.11 including support in LUInstanceSetParams
551   old_mod_item_fn = \
552     TAnd(TIsLength(2),
553          TItems([TOr(TElemOf(constants.DDMS_VALUES), TNonNegativeInt), fn]))
554
555   # New format, supporting adding/removing disks/NICs at arbitrary indices
556   mod_item_fn = \
557       TAnd(TIsLength(3), TItems([
558         TElemOf(constants.DDMS_VALUES_WITH_MODIFY),
559         Comment("Device index, can be negative, e.g. -1 for last disk")
560                  (TOr(TInt, TString)),
561         fn,
562         ]))
563
564   return TOr(Comment("Recommended")(TListOf(mod_item_fn)),
565              Comment("Deprecated")(TListOf(old_mod_item_fn)))
566
567
568 TINicParams = \
569     Comment("NIC parameters")(TDictOf(TElemOf(constants.INIC_PARAMS),
570                                       TMaybe(TString)))
571
572 TIDiskParams = \
573     Comment("Disk parameters")(TDictOf(TElemOf(constants.IDISK_PARAMS),
574                                        TOr(TNonEmptyString, TInt)))
575
576 THypervisor = TElemOf(constants.HYPER_TYPES)
577 TMigrationMode = TElemOf(constants.HT_MIGRATION_MODES)
578 TNICMode = TElemOf(constants.NIC_VALID_MODES)
579 TInstCreateMode = TElemOf(constants.INSTANCE_CREATE_MODES)
580 TRebootType = TElemOf(constants.REBOOT_TYPES)
581 TFileDriver = TElemOf(constants.FILE_DRIVER)
582 TOobCommand = TElemOf(constants.OOB_COMMANDS)
583 TQueryTypeOp = TElemOf(constants.QR_VIA_OP)
584
585 TDiskParams = \
586     Comment("Disk parameters")(TDictOf(TNonEmptyString,
587                                        TOr(TNonEmptyString, TInt)))
588
589 TDiskChanges = \
590     TAnd(TIsLength(2),
591          TItems([Comment("Disk index")(TNonNegativeInt),
592                  Comment("Parameters")(TDiskParams)]))
593
594 TRecreateDisksInfo = TOr(TListOf(TNonNegativeInt), TListOf(TDiskChanges))
595
596
597 def TStorageType(val):
598   """Builds a function that checks if a given value is a valid storage
599   type.
600
601   """
602   return (val in constants.STORAGE_TYPES)
603
604
605 TTagKind = TElemOf(constants.VALID_TAG_TYPES)
606 TDdmSimple = TElemOf(constants.DDMS_VALUES)
607 TVerifyOptionalChecks = TElemOf(constants.VERIFY_OPTIONAL_CHECKS)
608
609
610 @WithDesc("IPv4 network")
611 def _CheckCIDRNetNotation(value):
612   """Ensure a given CIDR notation type is valid.
613
614   """
615   try:
616     ipaddr.IPv4Network(value)
617   except ipaddr.AddressValueError:
618     return False
619   return True
620
621
622 @WithDesc("IPv4 address")
623 def _CheckCIDRAddrNotation(value):
624   """Ensure a given CIDR notation type is valid.
625
626   """
627   try:
628     ipaddr.IPv4Address(value)
629   except ipaddr.AddressValueError:
630     return False
631   return True
632
633
634 @WithDesc("IPv6 address")
635 def _CheckCIDR6AddrNotation(value):
636   """Ensure a given CIDR notation type is valid.
637
638   """
639   try:
640     ipaddr.IPv6Address(value)
641   except ipaddr.AddressValueError:
642     return False
643   return True
644
645
646 @WithDesc("IPv6 network")
647 def _CheckCIDR6NetNotation(value):
648   """Ensure a given CIDR notation type is valid.
649
650   """
651   try:
652     ipaddr.IPv6Network(value)
653   except ipaddr.AddressValueError:
654     return False
655   return True
656
657
658 TIPv4Address = TAnd(TString, _CheckCIDRAddrNotation)
659 TIPv6Address = TAnd(TString, _CheckCIDR6AddrNotation)
660 TIPv4Network = TAnd(TString, _CheckCIDRNetNotation)
661 TIPv6Network = TAnd(TString, _CheckCIDR6NetNotation)
662
663
664 def TObject(val_type):
665   return TDictOf(TAny, val_type)
666
667
668 def TObjectCheck(obj, fields_types):
669   """Helper to generate type checks for objects.
670
671   @param obj: The object to generate type checks
672   @param fields_types: The fields and their types as a dict
673   @return: A ht type check function
674
675   """
676   assert set(obj.GetAllSlots()) == set(fields_types.keys()), \
677     "%s != %s" % (set(obj.GetAllSlots()), set(fields_types.keys()))
678   return TStrictDict(True, True, fields_types)
679
680
681 TQueryFieldDef = \
682     TObjectCheck(objects.QueryFieldDefinition, {
683         "name": TNonEmptyString,
684         "title": TNonEmptyString,
685         "kind": TElemOf(constants.QFT_ALL),
686         "doc": TNonEmptyString
687     })
688
689 TQueryRow = \
690     TListOf(TAnd(TIsLength(2),
691                  TItems([TElemOf(constants.RS_ALL), TAny])))
692
693 TQueryResult = TListOf(TQueryRow)
694
695 TQueryResponse = \
696     TObjectCheck(objects.QueryResponse, {
697         "fields": TListOf(TQueryFieldDef),
698         "data": TQueryResult
699     })
700
701 TQueryFieldsResponse = \
702     TObjectCheck(objects.QueryFieldsResponse, {
703         "fields": TListOf(TQueryFieldDef)
704     })
705
706 TJobIdListItem = \
707     TAnd(TIsLength(2),
708          TItems([Comment("success")(TBool),
709                  Comment("Job ID if successful, error message"
710                          " otherwise")(TOr(TString, TJobId))]))
711
712 TJobIdList = TListOf(TJobIdListItem)
713
714 TJobIdListOnly = TStrictDict(True, True, {
715   constants.JOB_IDS_KEY: Comment("List of submitted jobs")(TJobIdList)
716   })
717
718 TInstanceMultiAllocResponse = \
719     TStrictDict(True, True, {
720       constants.JOB_IDS_KEY: Comment("List of submitted jobs")(TJobIdList),
721       constants.ALLOCATABLE_KEY: TListOf(TNonEmptyString),
722       constants.FAILED_KEY: TListOf(TNonEmptyString)
723     })