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