1# Copyright 2013 Red Hat, Inc. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may 4# not use this file except in compliance with the License. You may obtain 5# a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations 13# under the License. 14 15import abc 16from collections import abc as collections_abc 17import datetime 18from distutils import versionpredicate 19import re 20import uuid 21import warnings 22 23import copy 24import iso8601 25import netaddr 26from oslo_utils import strutils 27from oslo_utils import timeutils 28 29from oslo_versionedobjects._i18n import _ 30from oslo_versionedobjects import _utils 31from oslo_versionedobjects import exception 32 33 34class KeyTypeError(TypeError): 35 def __init__(self, expected, value): 36 super(KeyTypeError, self).__init__( 37 _('Key %(key)s must be of type %(expected)s not %(actual)s' 38 ) % {'key': repr(value), 39 'expected': expected.__name__, 40 'actual': value.__class__.__name__, 41 }) 42 43 44class ElementTypeError(TypeError): 45 def __init__(self, expected, key, value): 46 super(ElementTypeError, self).__init__( 47 _('Element %(key)s:%(val)s must be of type %(expected)s' 48 ' not %(actual)s' 49 ) % {'key': key, 50 'val': repr(value), 51 'expected': expected, 52 'actual': value.__class__.__name__, 53 }) 54 55 56class AbstractFieldType(object, metaclass=abc.ABCMeta): 57 @abc.abstractmethod 58 def coerce(self, obj, attr, value): 59 """This is called to coerce (if possible) a value on assignment. 60 61 This method should convert the value given into the designated type, 62 or throw an exception if this is not possible. 63 64 :param:obj: The VersionedObject on which an attribute is being set 65 :param:attr: The name of the attribute being set 66 :param:value: The value being set 67 :returns: A properly-typed value 68 """ 69 pass 70 71 @abc.abstractmethod 72 def from_primitive(self, obj, attr, value): 73 """This is called to deserialize a value. 74 75 This method should deserialize a value from the form given by 76 to_primitive() to the designated type. 77 78 :param:obj: The VersionedObject on which the value is to be set 79 :param:attr: The name of the attribute which will hold the value 80 :param:value: The serialized form of the value 81 :returns: The natural form of the value 82 """ 83 pass 84 85 @abc.abstractmethod 86 def to_primitive(self, obj, attr, value): 87 """This is called to serialize a value. 88 89 This method should serialize a value to the form expected by 90 from_primitive(). 91 92 :param:obj: The VersionedObject on which the value is set 93 :param:attr: The name of the attribute holding the value 94 :param:value: The natural form of the value 95 :returns: The serialized form of the value 96 """ 97 pass 98 99 @abc.abstractmethod 100 def describe(self): 101 """Returns a string describing the type of the field.""" 102 pass 103 104 @abc.abstractmethod 105 def stringify(self, value): 106 """Returns a short stringified version of a value.""" 107 pass 108 109 110class FieldType(AbstractFieldType): 111 @staticmethod 112 def coerce(obj, attr, value): 113 return value 114 115 @staticmethod 116 def from_primitive(obj, attr, value): 117 return value 118 119 @staticmethod 120 def to_primitive(obj, attr, value): 121 return value 122 123 def describe(self): 124 return self.__class__.__name__ 125 126 def stringify(self, value): 127 return str(value) 128 129 def get_schema(self): 130 raise NotImplementedError() 131 132 133class UnspecifiedDefault(object): 134 pass 135 136 137class Field(object): 138 def __init__(self, field_type, nullable=False, 139 default=UnspecifiedDefault, read_only=False): 140 self._type = field_type 141 self._nullable = nullable 142 self._default = default 143 self._read_only = read_only 144 145 def __repr__(self): 146 if isinstance(self._default, set): 147 # TODO(stephenfin): Drop this when we switch from 148 # 'inspect.getargspec' to 'inspect.getfullargspec', since our 149 # hashes will have to change anyway 150 # make a py27 and py35 compatible representation. See bug 1771804 151 default = 'set([%s])' % ','.join( 152 sorted([str(v) for v in self._default]) 153 ) 154 else: 155 default = str(self._default) 156 return '%s(default=%s,nullable=%s)' % (self._type.__class__.__name__, 157 default, self._nullable) 158 159 @property 160 def nullable(self): 161 return self._nullable 162 163 @property 164 def default(self): 165 return self._default 166 167 @property 168 def read_only(self): 169 return self._read_only 170 171 def _null(self, obj, attr): 172 if self.nullable: 173 return None 174 elif self._default != UnspecifiedDefault: 175 # NOTE(danms): We coerce the default value each time the field 176 # is set to None as our contract states that we'll let the type 177 # examine the object and attribute name at that time. 178 return self._type.coerce(obj, attr, copy.deepcopy(self._default)) 179 else: 180 raise ValueError(_("Field `%s' cannot be None") % attr) 181 182 def coerce(self, obj, attr, value): 183 """Coerce a value to a suitable type. 184 185 This is called any time you set a value on an object, like: 186 187 foo.myint = 1 188 189 and is responsible for making sure that the value (1 here) is of 190 the proper type, or can be sanely converted. 191 192 This also handles the potentially nullable or defaultable 193 nature of the field and calls the coerce() method on a 194 FieldType to actually do the coercion. 195 196 :param:obj: The object being acted upon 197 :param:attr: The name of the attribute/field being set 198 :param:value: The value being set 199 :returns: The properly-typed value 200 """ 201 if value is None: 202 return self._null(obj, attr) 203 else: 204 return self._type.coerce(obj, attr, value) 205 206 def from_primitive(self, obj, attr, value): 207 """Deserialize a value from primitive form. 208 209 This is responsible for deserializing a value from primitive 210 into regular form. It calls the from_primitive() method on a 211 FieldType to do the actual deserialization. 212 213 :param:obj: The object being acted upon 214 :param:attr: The name of the attribute/field being deserialized 215 :param:value: The value to be deserialized 216 :returns: The deserialized value 217 """ 218 if value is None: 219 return None 220 else: 221 return self._type.from_primitive(obj, attr, value) 222 223 def to_primitive(self, obj, attr, value): 224 """Serialize a value to primitive form. 225 226 This is responsible for serializing a value to primitive 227 form. It calls to_primitive() on a FieldType to do the actual 228 serialization. 229 230 :param:obj: The object being acted upon 231 :param:attr: The name of the attribute/field being serialized 232 :param:value: The value to be serialized 233 :returns: The serialized value 234 """ 235 if value is None: 236 return None 237 else: 238 return self._type.to_primitive(obj, attr, value) 239 240 def describe(self): 241 """Return a short string describing the type of this field.""" 242 name = self._type.describe() 243 prefix = self.nullable and 'Nullable' or '' 244 return prefix + name 245 246 def stringify(self, value): 247 if value is None: 248 return 'None' 249 else: 250 return self._type.stringify(value) 251 252 def get_schema(self): 253 schema = self._type.get_schema() 254 schema.update({'readonly': self.read_only}) 255 if self.nullable: 256 schema['type'].append('null') 257 default = self.default 258 if default != UnspecifiedDefault: 259 schema.update({'default': default}) 260 return schema 261 262 263class String(FieldType): 264 @staticmethod 265 def coerce(obj, attr, value): 266 # FIXME(danms): We should really try to avoid the need to do this 267 accepted_types = (int, float, str, datetime.datetime) 268 if isinstance(value, accepted_types): 269 return str(value) 270 271 raise ValueError(_('A string is required in field %(attr)s, ' 272 'not a %(type)s') % 273 {'attr': attr, 'type': type(value).__name__}) 274 275 @staticmethod 276 def stringify(value): 277 return '\'%s\'' % value 278 279 def get_schema(self): 280 return {'type': ['string']} 281 282 283class SensitiveString(String): 284 """A string field type that may contain sensitive (password) information. 285 286 Passwords in the string value are masked when stringified. 287 """ 288 def stringify(self, value): 289 return super(SensitiveString, self).stringify( 290 strutils.mask_password(value)) 291 292 293class VersionPredicate(String): 294 @staticmethod 295 def coerce(obj, attr, value): 296 try: 297 versionpredicate.VersionPredicate('check (%s)' % value) 298 except ValueError: 299 raise ValueError(_('Version %(val)s is not a valid predicate in ' 300 'field %(attr)s') % 301 {'val': value, 'attr': attr}) 302 return value 303 304 305class Enum(String): 306 def __init__(self, valid_values, **kwargs): 307 if not valid_values: 308 raise exception.EnumRequiresValidValuesError() 309 try: 310 # Test validity of the values 311 for value in valid_values: 312 super(Enum, self).coerce(None, 'init', value) 313 except (TypeError, ValueError): 314 raise exception.EnumValidValuesInvalidError() 315 self._valid_values = valid_values 316 super(Enum, self).__init__(**kwargs) 317 318 @property 319 def valid_values(self): 320 return copy.copy(self._valid_values) 321 322 def coerce(self, obj, attr, value): 323 if value not in self._valid_values: 324 msg = _("Field value %s is invalid") % value 325 raise ValueError(msg) 326 return super(Enum, self).coerce(obj, attr, value) 327 328 def stringify(self, value): 329 if value not in self._valid_values: 330 msg = _("Field value %s is invalid") % value 331 raise ValueError(msg) 332 return super(Enum, self).stringify(value) 333 334 def get_schema(self): 335 schema = super(Enum, self).get_schema() 336 schema['enum'] = self._valid_values 337 return schema 338 339 340class StringPattern(FieldType): 341 def get_schema(self): 342 if hasattr(self, "PATTERN"): 343 return {'type': ['string'], 'pattern': self.PATTERN} 344 else: 345 msg = _("%s has no pattern") % self.__class__.__name__ 346 raise AttributeError(msg) 347 348 349class UUID(StringPattern): 350 351 PATTERN = (r'^[a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]' 352 r'{4}-?[a-fA-F0-9]{12}$') 353 354 @staticmethod 355 def coerce(obj, attr, value): 356 # FIXME(danms): We should actually verify the UUIDness here 357 with warnings.catch_warnings(): 358 # Change the warning action only if no other filter exists 359 # for this warning to allow the client to define other action 360 # like 'error' for this warning. 361 warnings.filterwarnings(action="once", append=True) 362 try: 363 uuid.UUID("%s" % value) 364 except Exception: 365 # This is to ensure no breaking behaviour for current 366 # users 367 warnings.warn("%s is an invalid UUID. Using UUIDFields " 368 "with invalid UUIDs is no longer " 369 "supported, and will be removed in a future " 370 "release. Please update your " 371 "code to input valid UUIDs or accept " 372 "ValueErrors for invalid UUIDs. See " 373 "https://docs.openstack.org/oslo.versionedobjects/latest/reference/fields.html#oslo_versionedobjects.fields.UUIDField " # noqa 374 "for further details" % 375 repr(value).encode('utf8'), 376 FutureWarning) 377 378 return "%s" % value 379 380 381class MACAddress(StringPattern): 382 383 PATTERN = r'^[0-9a-f]{2}(:[0-9a-f]{2}){5}$' 384 _REGEX = re.compile(PATTERN) 385 386 @staticmethod 387 def coerce(obj, attr, value): 388 if isinstance(value, str): 389 lowered = value.lower().replace('-', ':') 390 if MACAddress._REGEX.match(lowered): 391 return lowered 392 raise ValueError(_("Malformed MAC %s") % (value,)) 393 394 395class PCIAddress(StringPattern): 396 397 PATTERN = r'^[0-9a-f]{4}:[0-9a-f]{2}:[0-1][0-9a-f].[0-7]$' 398 _REGEX = re.compile(PATTERN) 399 400 @staticmethod 401 def coerce(obj, attr, value): 402 if isinstance(value, str): 403 newvalue = value.lower() 404 if PCIAddress._REGEX.match(newvalue): 405 return newvalue 406 raise ValueError(_("Malformed PCI address %s") % (value,)) 407 408 409class Integer(FieldType): 410 @staticmethod 411 def coerce(obj, attr, value): 412 return int(value) 413 414 def get_schema(self): 415 return {'type': ['integer']} 416 417 418class NonNegativeInteger(FieldType): 419 @staticmethod 420 def coerce(obj, attr, value): 421 v = int(value) 422 if v < 0: 423 raise ValueError(_('Value must be >= 0 for field %s') % attr) 424 return v 425 426 def get_schema(self): 427 return {'type': ['integer'], 'minimum': 0} 428 429 430class Float(FieldType): 431 def coerce(self, obj, attr, value): 432 return float(value) 433 434 def get_schema(self): 435 return {'type': ['number']} 436 437 438class NonNegativeFloat(FieldType): 439 @staticmethod 440 def coerce(obj, attr, value): 441 v = float(value) 442 if v < 0: 443 raise ValueError(_('Value must be >= 0 for field %s') % attr) 444 return v 445 446 def get_schema(self): 447 return {'type': ['number'], 'minimum': 0} 448 449 450class Boolean(FieldType): 451 @staticmethod 452 def coerce(obj, attr, value): 453 return bool(value) 454 455 def get_schema(self): 456 return {'type': ['boolean']} 457 458 459class FlexibleBoolean(Boolean): 460 @staticmethod 461 def coerce(obj, attr, value): 462 return strutils.bool_from_string(value) 463 464 465class DateTime(FieldType): 466 def __init__(self, tzinfo_aware=True, *args, **kwargs): 467 self.tzinfo_aware = tzinfo_aware 468 super(DateTime, self).__init__(*args, **kwargs) 469 470 def coerce(self, obj, attr, value): 471 if isinstance(value, str): 472 # NOTE(danms): Being tolerant of isotime strings here will help us 473 # during our objects transition 474 value = timeutils.parse_isotime(value) 475 elif not isinstance(value, datetime.datetime): 476 raise ValueError(_('A datetime.datetime is required ' 477 'in field %(attr)s, not a %(type)s') % 478 {'attr': attr, 'type': type(value).__name__}) 479 480 if value.utcoffset() is None and self.tzinfo_aware: 481 # NOTE(danms): Legacy objects from sqlalchemy are stored in UTC, 482 # but are returned without a timezone attached. 483 # As a transitional aid, assume a tz-naive object is in UTC. 484 value = value.replace(tzinfo=iso8601.UTC) 485 elif not self.tzinfo_aware: 486 value = value.replace(tzinfo=None) 487 return value 488 489 def from_primitive(self, obj, attr, value): 490 return self.coerce(obj, attr, timeutils.parse_isotime(value)) 491 492 def get_schema(self): 493 return {'type': ['string'], 'format': 'date-time'} 494 495 @staticmethod 496 def to_primitive(obj, attr, value): 497 return _utils.isotime(value) 498 499 @staticmethod 500 def stringify(value): 501 return _utils.isotime(value) 502 503 504class IPAddress(StringPattern): 505 @staticmethod 506 def coerce(obj, attr, value): 507 try: 508 return netaddr.IPAddress(value) 509 except netaddr.AddrFormatError as e: 510 raise ValueError(str(e)) 511 512 def from_primitive(self, obj, attr, value): 513 return self.coerce(obj, attr, value) 514 515 @staticmethod 516 def to_primitive(obj, attr, value): 517 return str(value) 518 519 520class IPV4Address(IPAddress): 521 @staticmethod 522 def coerce(obj, attr, value): 523 result = IPAddress.coerce(obj, attr, value) 524 if result.version != 4: 525 raise ValueError(_('Network "%(val)s" is not valid ' 526 'in field %(attr)s') % 527 {'val': value, 'attr': attr}) 528 return result 529 530 def get_schema(self): 531 return {'type': ['string'], 'format': 'ipv4'} 532 533 534class IPV6Address(IPAddress): 535 @staticmethod 536 def coerce(obj, attr, value): 537 result = IPAddress.coerce(obj, attr, value) 538 if result.version != 6: 539 raise ValueError(_('Network "%(val)s" is not valid ' 540 'in field %(attr)s') % 541 {'val': value, 'attr': attr}) 542 return result 543 544 def get_schema(self): 545 return {'type': ['string'], 'format': 'ipv6'} 546 547 548class IPV4AndV6Address(IPAddress): 549 @staticmethod 550 def coerce(obj, attr, value): 551 result = IPAddress.coerce(obj, attr, value) 552 if result.version != 4 and result.version != 6: 553 raise ValueError(_('Network "%(val)s" is not valid ' 554 'in field %(attr)s') % 555 {'val': value, 'attr': attr}) 556 return result 557 558 def get_schema(self): 559 return {'oneOf': [IPV4Address().get_schema(), 560 IPV6Address().get_schema()]} 561 562 563class IPNetwork(IPAddress): 564 @staticmethod 565 def coerce(obj, attr, value): 566 try: 567 return netaddr.IPNetwork(value) 568 except netaddr.AddrFormatError as e: 569 raise ValueError(str(e)) 570 571 572class IPV4Network(IPNetwork): 573 574 PATTERN = (r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-' 575 r'9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][' 576 r'0-9]|3[0-2]))$') 577 578 @staticmethod 579 def coerce(obj, attr, value): 580 try: 581 return netaddr.IPNetwork(value, version=4) 582 except netaddr.AddrFormatError as e: 583 raise ValueError(str(e)) 584 585 586class IPV6Network(IPNetwork): 587 588 def __init__(self, *args, **kwargs): 589 super(IPV6Network, self).__init__(*args, **kwargs) 590 self.PATTERN = self._create_pattern() 591 592 @staticmethod 593 def coerce(obj, attr, value): 594 try: 595 return netaddr.IPNetwork(value, version=6) 596 except netaddr.AddrFormatError as e: 597 raise ValueError(str(e)) 598 599 def _create_pattern(self): 600 ipv6seg = '[0-9a-fA-F]{1,4}' 601 ipv4seg = '(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])' 602 603 return ( 604 # Pattern based on answer to 605 # http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses 606 '^' 607 # 1:2:3:4:5:6:7:8 608 '(' + ipv6seg + ':){7,7}' + ipv6seg + '|' 609 # 1:: 1:2:3:4:5:6:7:: 610 '(' + ipv6seg + ':){1,7}:|' 611 # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 612 '(' + ipv6seg + ':){1,6}:' + ipv6seg + '|' 613 # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 614 '(' + ipv6seg + ':){1,5}(:' + ipv6seg + '){1,2}|' 615 # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 616 '(' + ipv6seg + ':){1,4}(:' + ipv6seg + '){1,3}|' 617 # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 618 '(' + ipv6seg + ':){1,3}(:' + ipv6seg + '){1,4}|' 619 # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 620 '(' + ipv6seg + ':){1,2}(:' + ipv6seg + '){1,5}|' + 621 # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 622 ipv6seg + ':((:' + ipv6seg + '){1,6})|' 623 # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: 624 ':((:' + ipv6seg + '){1,7}|:)|' 625 # fe80::7:8%eth0 fe80::7:8%1 626 'fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|' 627 # ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 628 '::(ffff(:0{1,4}){0,1}:){0,1}' 629 '(' + ipv4seg + r'\.){3,3}' + 630 ipv4seg + '|' 631 # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 632 '(' + ipv6seg + ':){1,4}:' 633 '(' + ipv4seg + r'\.){3,3}' + 634 ipv4seg + 635 # /128 636 r'(\/(d|dd|1[0-1]d|12[0-8]))$' 637 ) 638 639 640class CompoundFieldType(FieldType): 641 def __init__(self, element_type, **field_args): 642 self._element_type = Field(element_type, **field_args) 643 644 645class List(CompoundFieldType): 646 def coerce(self, obj, attr, value): 647 648 if (not isinstance(value, collections_abc.Iterable) or 649 isinstance(value, (str, collections_abc.Mapping))): 650 raise ValueError(_('A list is required in field %(attr)s, ' 651 'not a %(type)s') % 652 {'attr': attr, 'type': type(value).__name__}) 653 coerced_list = CoercedList() 654 coerced_list.enable_coercing(self._element_type, obj, attr) 655 coerced_list.extend(value) 656 return coerced_list 657 658 def to_primitive(self, obj, attr, value): 659 return [self._element_type.to_primitive(obj, attr, x) for x in value] 660 661 def from_primitive(self, obj, attr, value): 662 return [self._element_type.from_primitive(obj, attr, x) for x in value] 663 664 def stringify(self, value): 665 return '[%s]' % ( 666 ','.join([self._element_type.stringify(x) for x in value])) 667 668 def get_schema(self): 669 return {'type': ['array'], 'items': self._element_type.get_schema()} 670 671 672class Dict(CompoundFieldType): 673 def coerce(self, obj, attr, value): 674 if not isinstance(value, dict): 675 raise ValueError(_('A dict is required in field %(attr)s, ' 676 'not a %(type)s') % 677 {'attr': attr, 'type': type(value).__name__}) 678 coerced_dict = CoercedDict() 679 coerced_dict.enable_coercing(self._element_type, obj, attr) 680 coerced_dict.update(value) 681 return coerced_dict 682 683 def to_primitive(self, obj, attr, value): 684 primitive = {} 685 for key, element in value.items(): 686 primitive[key] = self._element_type.to_primitive( 687 obj, '%s["%s"]' % (attr, key), element) 688 return primitive 689 690 def from_primitive(self, obj, attr, value): 691 concrete = {} 692 for key, element in value.items(): 693 concrete[key] = self._element_type.from_primitive( 694 obj, '%s["%s"]' % (attr, key), element) 695 return concrete 696 697 def stringify(self, value): 698 return '{%s}' % ( 699 ','.join(['%s=%s' % (key, self._element_type.stringify(val)) 700 for key, val in sorted(value.items())])) 701 702 def get_schema(self): 703 return {'type': ['object'], 704 'additionalProperties': self._element_type.get_schema()} 705 706 707class DictProxyField(object): 708 """Descriptor allowing us to assign pinning data as a dict of key_types 709 710 This allows us to have an object field that will be a dict of key_type 711 keys, allowing that will convert back to string-keyed dict. 712 713 This will take care of the conversion while the dict field will make sure 714 that we store the raw json-serializable data on the object. 715 716 key_type should return a type that unambiguously responds to str 717 so that calling key_type on it yields the same thing. 718 """ 719 def __init__(self, dict_field_name, key_type=int): 720 self._fld_name = dict_field_name 721 self._key_type = key_type 722 723 def __get__(self, obj, obj_type): 724 if obj is None: 725 return self 726 if getattr(obj, self._fld_name) is None: 727 return 728 return {self._key_type(k): v 729 for k, v in getattr(obj, self._fld_name).items()} 730 731 def __set__(self, obj, val): 732 if val is None: 733 setattr(obj, self._fld_name, val) 734 else: 735 setattr(obj, self._fld_name, {str(k): v for k, v in val.items()}) 736 737 738class Set(CompoundFieldType): 739 def coerce(self, obj, attr, value): 740 if not isinstance(value, set): 741 raise ValueError(_('A set is required in field %(attr)s, ' 742 'not a %(type)s') % 743 {'attr': attr, 'type': type(value).__name__}) 744 coerced_set = CoercedSet() 745 coerced_set.enable_coercing(self._element_type, obj, attr) 746 coerced_set.update(value) 747 return coerced_set 748 749 def to_primitive(self, obj, attr, value): 750 return tuple( 751 self._element_type.to_primitive(obj, attr, x) for x in value) 752 753 def from_primitive(self, obj, attr, value): 754 return set([self._element_type.from_primitive(obj, attr, x) 755 for x in value]) 756 757 def stringify(self, value): 758 return 'set([%s])' % ( 759 ','.join([self._element_type.stringify(x) for x in value])) 760 761 def get_schema(self): 762 return {'type': ['array'], 'uniqueItems': True, 763 'items': self._element_type.get_schema()} 764 765 766class Object(FieldType): 767 def __init__(self, obj_name, subclasses=False, **kwargs): 768 self._obj_name = obj_name 769 self._subclasses = subclasses 770 super(Object, self).__init__(**kwargs) 771 772 @staticmethod 773 def _get_all_obj_names(obj): 774 obj_names = [] 775 for parent in obj.__class__.mro(): 776 # Skip mix-ins which are not versioned object subclasses 777 if not hasattr(parent, "obj_name"): 778 continue 779 obj_names.append(parent.obj_name()) 780 return obj_names 781 782 def coerce(self, obj, attr, value): 783 try: 784 obj_name = value.obj_name() 785 except AttributeError: 786 obj_name = "" 787 788 if self._subclasses: 789 obj_names = self._get_all_obj_names(value) 790 else: 791 obj_names = [obj_name] 792 793 if self._obj_name not in obj_names: 794 if not obj_name: 795 # If we're not dealing with an object, it's probably a 796 # primitive so get it's type for the message below. 797 obj_name = type(value).__name__ 798 obj_mod = '' 799 if hasattr(obj, '__module__'): 800 obj_mod = ''.join([obj.__module__, '.']) 801 val_mod = '' 802 if hasattr(value, '__module__'): 803 val_mod = ''.join([value.__module__, '.']) 804 raise ValueError(_('An object of type %(type)s is required ' 805 'in field %(attr)s, not a %(valtype)s') % 806 {'type': ''.join([obj_mod, self._obj_name]), 807 'attr': attr, 'valtype': ''.join([val_mod, 808 obj_name])}) 809 return value 810 811 @staticmethod 812 def to_primitive(obj, attr, value): 813 return value.obj_to_primitive() 814 815 @staticmethod 816 def from_primitive(obj, attr, value): 817 # FIXME(danms): Avoid circular import from base.py 818 from oslo_versionedobjects import base as obj_base 819 # NOTE (ndipanov): If they already got hydrated by the serializer, just 820 # pass them back unchanged 821 if isinstance(value, obj_base.VersionedObject): 822 return value 823 return obj.obj_from_primitive(value, obj._context) 824 825 def describe(self): 826 return "Object<%s>" % self._obj_name 827 828 def stringify(self, value): 829 if 'uuid' in value.fields: 830 ident = '(%s)' % (value.obj_attr_is_set('uuid') and value.uuid or 831 'UNKNOWN') 832 elif 'id' in value.fields: 833 ident = '(%s)' % (value.obj_attr_is_set('id') and value.id or 834 'UNKNOWN') 835 else: 836 ident = '' 837 838 return '%s%s' % (value.obj_name(), ident) 839 840 def get_schema(self): 841 from oslo_versionedobjects import base as obj_base 842 obj_classes = obj_base.VersionedObjectRegistry.obj_classes() 843 if self._obj_name in obj_classes: 844 cls = obj_classes[self._obj_name][0] 845 namespace_key = cls._obj_primitive_key('namespace') 846 name_key = cls._obj_primitive_key('name') 847 version_key = cls._obj_primitive_key('version') 848 data_key = cls._obj_primitive_key('data') 849 changes_key = cls._obj_primitive_key('changes') 850 field_schemas = {key: field.get_schema() 851 for key, field in cls.fields.items()} 852 required_fields = [key for key, field in sorted(cls.fields.items()) 853 if not field.nullable] 854 schema = { 855 'type': ['object'], 856 'properties': { 857 namespace_key: { 858 'type': 'string' 859 }, 860 name_key: { 861 'type': 'string' 862 }, 863 version_key: { 864 'type': 'string' 865 }, 866 changes_key: { 867 'type': 'array', 868 'items': { 869 'type': 'string' 870 } 871 }, 872 data_key: { 873 'type': 'object', 874 'description': 'fields of %s' % self._obj_name, 875 'properties': field_schemas, 876 }, 877 }, 878 'required': [namespace_key, name_key, version_key, data_key] 879 } 880 881 if required_fields: 882 schema['properties'][data_key]['required'] = required_fields 883 884 return schema 885 else: 886 raise exception.UnsupportedObjectError(objtype=self._obj_name) 887 888 889class AutoTypedField(Field): 890 AUTO_TYPE = None 891 892 def __init__(self, **kwargs): 893 super(AutoTypedField, self).__init__(self.AUTO_TYPE, **kwargs) 894 895 896class StringField(AutoTypedField): 897 AUTO_TYPE = String() 898 899 900class SensitiveStringField(AutoTypedField): 901 """Field type that masks passwords when the field is stringified.""" 902 AUTO_TYPE = SensitiveString() 903 904 905class VersionPredicateField(AutoTypedField): 906 AUTO_TYPE = VersionPredicate() 907 908 909class BaseEnumField(AutoTypedField): 910 '''Base class for all enum field types 911 912 This class should not be directly instantiated. Instead 913 subclass it and set AUTO_TYPE to be a SomeEnum() 914 where SomeEnum is a subclass of Enum. 915 ''' 916 917 def __init__(self, **kwargs): 918 if self.AUTO_TYPE is None: 919 raise exception.EnumFieldUnset( 920 fieldname=self.__class__.__name__) 921 922 if not isinstance(self.AUTO_TYPE, Enum): 923 raise exception.EnumFieldInvalid( 924 typename=self.AUTO_TYPE.__class__.__name__, 925 fieldname=self.__class__.__name__) 926 927 super(BaseEnumField, self).__init__(**kwargs) 928 929 def __repr__(self): 930 valid_values = self._type.valid_values 931 args = { 932 'nullable': self._nullable, 933 'default': self._default, 934 } 935 args.update({'valid_values': valid_values}) 936 return '%s(%s)' % (self._type.__class__.__name__, 937 ','.join(['%s=%s' % (k, v) 938 for k, v in sorted(args.items())])) 939 940 @property 941 def valid_values(self): 942 """Return the list of valid values for the field.""" 943 return self._type.valid_values 944 945 946class EnumField(BaseEnumField): 947 '''Anonymous enum field type 948 949 This class allows for anonymous enum types to be 950 declared, simply by passing in a list of valid values 951 to its constructor. It is generally preferable though, 952 to create an explicit named enum type by sub-classing 953 the BaseEnumField type directly. 954 ''' 955 956 def __init__(self, valid_values, **kwargs): 957 self.AUTO_TYPE = Enum(valid_values=valid_values) 958 super(EnumField, self).__init__(**kwargs) 959 960 961class StateMachine(EnumField): 962 """A mixin that can be applied to an EnumField to enforce a state machine 963 964 e.g: Setting the code below on a field will ensure an object cannot 965 transition from ERROR to ACTIVE 966 967 :example: 968 .. code-block:: python 969 970 class FakeStateMachineField(fields.EnumField, fields.StateMachine): 971 972 ACTIVE = 'ACTIVE' 973 PENDING = 'PENDING' 974 ERROR = 'ERROR' 975 DELETED = 'DELETED' 976 977 ALLOWED_TRANSITIONS = { 978 ACTIVE: { 979 PENDING, 980 ERROR, 981 DELETED, 982 }, 983 PENDING: { 984 ACTIVE, 985 ERROR 986 }, 987 ERROR: { 988 PENDING, 989 }, 990 DELETED: {} # This is a terminal state 991 } 992 993 _TYPES = (ACTIVE, PENDING, ERROR, DELETED) 994 995 def __init__(self, **kwargs): 996 super(FakeStateMachineField, self).__init__( 997 self._TYPES, **kwargs) 998 999 """ 1000 # This is dict of states, that have dicts of states an object is 1001 # allowed to transition to 1002 1003 ALLOWED_TRANSITIONS = {} 1004 1005 def _my_name(self, obj): 1006 for name, field in obj.fields.items(): 1007 if field == self: 1008 return name 1009 return 'unknown' 1010 1011 def coerce(self, obj, attr, value): 1012 super(StateMachine, self).coerce(obj, attr, value) 1013 my_name = self._my_name(obj) 1014 msg = _("%(object)s.%(name)s is not allowed to transition out of " 1015 "%(value)s state") 1016 1017 if attr in obj: 1018 current_value = getattr(obj, attr) 1019 else: 1020 return value 1021 1022 if current_value in self.ALLOWED_TRANSITIONS: 1023 1024 if value in self.ALLOWED_TRANSITIONS[current_value]: 1025 return value 1026 else: 1027 msg = _( 1028 "%(object)s.%(name)s is not allowed to transition out of " 1029 "'%(current_value)s' state to '%(value)s' state, choose " 1030 "from %(options)r") 1031 msg = msg % { 1032 'object': obj.obj_name(), 1033 'name': my_name, 1034 'current_value': current_value, 1035 'value': value, 1036 'options': [x for x in self.ALLOWED_TRANSITIONS[current_value]] 1037 } 1038 raise ValueError(msg) 1039 1040 1041class UUIDField(AutoTypedField): 1042 """UUID Field Type 1043 1044 .. warning:: 1045 1046 This class does not actually validate UUIDs. This will happen in a 1047 future major version of oslo.versionedobjects 1048 1049 To validate that you have valid UUIDs you need to do the following in 1050 your own objects/fields.py 1051 1052 :Example: 1053 .. code-block:: python 1054 1055 import oslo_versionedobjects.fields as ovo_fields 1056 1057 class UUID(ovo_fields.UUID): 1058 def coerce(self, obj, attr, value): 1059 uuid.UUID(value) 1060 return str(value) 1061 1062 1063 class UUIDField(ovo_fields.AutoTypedField): 1064 AUTO_TYPE = UUID() 1065 1066 and then in your objects use 1067 ``<your_projects>.object.fields.UUIDField``. 1068 1069 This will become default behaviour in the future. 1070 """ 1071 AUTO_TYPE = UUID() 1072 1073 1074class MACAddressField(AutoTypedField): 1075 AUTO_TYPE = MACAddress() 1076 1077 1078class PCIAddressField(AutoTypedField): 1079 AUTO_TYPE = PCIAddress() 1080 1081 1082class IntegerField(AutoTypedField): 1083 AUTO_TYPE = Integer() 1084 1085 1086class NonNegativeIntegerField(AutoTypedField): 1087 AUTO_TYPE = NonNegativeInteger() 1088 1089 1090class FloatField(AutoTypedField): 1091 AUTO_TYPE = Float() 1092 1093 1094class NonNegativeFloatField(AutoTypedField): 1095 AUTO_TYPE = NonNegativeFloat() 1096 1097 1098# This is a strict interpretation of boolean 1099# values using Python's semantics for truth/falsehood 1100class BooleanField(AutoTypedField): 1101 AUTO_TYPE = Boolean() 1102 1103 1104# This is a flexible interpretation of boolean 1105# values using common user friendly semantics for 1106# truth/falsehood. ie strings like 'yes', 'no', 1107# 'on', 'off', 't', 'f' get mapped to values you 1108# would expect. 1109class FlexibleBooleanField(AutoTypedField): 1110 AUTO_TYPE = FlexibleBoolean() 1111 1112 1113class DateTimeField(AutoTypedField): 1114 def __init__(self, tzinfo_aware=True, **kwargs): 1115 self.AUTO_TYPE = DateTime(tzinfo_aware=tzinfo_aware) 1116 super(DateTimeField, self).__init__(**kwargs) 1117 1118 1119class DictOfStringsField(AutoTypedField): 1120 AUTO_TYPE = Dict(String()) 1121 1122 1123class DictOfNullableStringsField(AutoTypedField): 1124 AUTO_TYPE = Dict(String(), nullable=True) 1125 1126 1127class DictOfIntegersField(AutoTypedField): 1128 AUTO_TYPE = Dict(Integer()) 1129 1130 1131class ListOfStringsField(AutoTypedField): 1132 AUTO_TYPE = List(String()) 1133 1134 1135class DictOfListOfStringsField(AutoTypedField): 1136 AUTO_TYPE = Dict(List(String())) 1137 1138 1139class ListOfEnumField(AutoTypedField): 1140 def __init__(self, valid_values, **kwargs): 1141 self.AUTO_TYPE = List(Enum(valid_values)) 1142 super(ListOfEnumField, self).__init__(**kwargs) 1143 1144 def __repr__(self): 1145 valid_values = self._type._element_type._type.valid_values 1146 args = { 1147 'nullable': self._nullable, 1148 'default': self._default, 1149 } 1150 args.update({'valid_values': valid_values}) 1151 return '%s(%s)' % (self._type.__class__.__name__, 1152 ','.join(['%s=%s' % (k, v) 1153 for k, v in sorted(args.items())])) 1154 1155 1156class SetOfIntegersField(AutoTypedField): 1157 AUTO_TYPE = Set(Integer()) 1158 1159 1160class ListOfSetsOfIntegersField(AutoTypedField): 1161 AUTO_TYPE = List(Set(Integer())) 1162 1163 1164class ListOfIntegersField(AutoTypedField): 1165 AUTO_TYPE = List(Integer()) 1166 1167 1168class ListOfDictOfNullableStringsField(AutoTypedField): 1169 AUTO_TYPE = List(Dict(String(), nullable=True)) 1170 1171 1172class ObjectField(AutoTypedField): 1173 def __init__(self, objtype, subclasses=False, **kwargs): 1174 self.AUTO_TYPE = Object(objtype, subclasses) 1175 self.objname = objtype 1176 super(ObjectField, self).__init__(**kwargs) 1177 1178 1179class ListOfObjectsField(AutoTypedField): 1180 def __init__(self, objtype, subclasses=False, **kwargs): 1181 self.AUTO_TYPE = List(Object(objtype, subclasses)) 1182 self.objname = objtype 1183 super(ListOfObjectsField, self).__init__(**kwargs) 1184 1185 1186class ListOfUUIDField(AutoTypedField): 1187 AUTO_TYPE = List(UUID()) 1188 1189 1190class IPAddressField(AutoTypedField): 1191 AUTO_TYPE = IPAddress() 1192 1193 1194class IPV4AddressField(AutoTypedField): 1195 AUTO_TYPE = IPV4Address() 1196 1197 1198class IPV6AddressField(AutoTypedField): 1199 AUTO_TYPE = IPV6Address() 1200 1201 1202class IPV4AndV6AddressField(AutoTypedField): 1203 AUTO_TYPE = IPV4AndV6Address() 1204 1205 1206class IPNetworkField(AutoTypedField): 1207 AUTO_TYPE = IPNetwork() 1208 1209 1210class IPV4NetworkField(AutoTypedField): 1211 AUTO_TYPE = IPV4Network() 1212 1213 1214class IPV6NetworkField(AutoTypedField): 1215 AUTO_TYPE = IPV6Network() 1216 1217 1218class CoercedCollectionMixin(object): 1219 def __init__(self, *args, **kwargs): 1220 self._element_type = None 1221 self._obj = None 1222 self._field = None 1223 super(CoercedCollectionMixin, self).__init__(*args, **kwargs) 1224 1225 def enable_coercing(self, element_type, obj, field): 1226 self._element_type = element_type 1227 self._obj = obj 1228 self._field = field 1229 1230 1231class CoercedList(CoercedCollectionMixin, list): 1232 """List which coerces its elements 1233 1234 List implementation which overrides all element-adding methods and 1235 coercing the element(s) being added to the required element type 1236 """ 1237 def _coerce_item(self, index, item): 1238 if hasattr(self, "_element_type") and self._element_type is not None: 1239 att_name = "%s[%i]" % (self._field, index) 1240 return self._element_type.coerce(self._obj, att_name, item) 1241 else: 1242 return item 1243 1244 def __setitem__(self, i, y): 1245 if type(i) is slice: # compatibility with py3 and [::] slices 1246 start = i.start or 0 1247 step = i.step or 1 1248 coerced_items = [self._coerce_item(start + index * step, item) 1249 for index, item in enumerate(y)] 1250 super(CoercedList, self).__setitem__(i, coerced_items) 1251 else: 1252 super(CoercedList, self).__setitem__(i, self._coerce_item(i, y)) 1253 1254 def append(self, x): 1255 super(CoercedList, self).append(self._coerce_item(len(self) + 1, x)) 1256 1257 def extend(self, t): 1258 coerced_items = [self._coerce_item(len(self) + index, item) 1259 for index, item in enumerate(t)] 1260 super(CoercedList, self).extend(coerced_items) 1261 1262 def insert(self, i, x): 1263 super(CoercedList, self).insert(i, self._coerce_item(i, x)) 1264 1265 def __iadd__(self, y): 1266 coerced_items = [self._coerce_item(len(self) + index, item) 1267 for index, item in enumerate(y)] 1268 return super(CoercedList, self).__iadd__(coerced_items) 1269 1270 def __setslice__(self, i, j, y): 1271 coerced_items = [self._coerce_item(i + index, item) 1272 for index, item in enumerate(y)] 1273 return super(CoercedList, self).__setslice__(i, j, coerced_items) 1274 1275 1276class CoercedDict(CoercedCollectionMixin, dict): 1277 """Dict which coerces its values 1278 1279 Dict implementation which overrides all element-adding methods and 1280 coercing the element(s) being added to the required element type 1281 """ 1282 1283 def _coerce_dict(self, d): 1284 res = {} 1285 for key, element in d.items(): 1286 res[key] = self._coerce_item(key, element) 1287 return res 1288 1289 def _coerce_item(self, key, item): 1290 if not isinstance(key, str): 1291 raise KeyTypeError(str, key) 1292 if hasattr(self, "_element_type") and self._element_type is not None: 1293 att_name = "%s[%s]" % (self._field, key) 1294 return self._element_type.coerce(self._obj, att_name, item) 1295 else: 1296 return item 1297 1298 def __setitem__(self, key, value): 1299 super(CoercedDict, self).__setitem__(key, 1300 self._coerce_item(key, value)) 1301 1302 def update(self, other=None, **kwargs): 1303 if other is not None: 1304 super(CoercedDict, self).update(self._coerce_dict(other), 1305 **self._coerce_dict(kwargs)) 1306 else: 1307 super(CoercedDict, self).update(**self._coerce_dict(kwargs)) 1308 1309 def setdefault(self, key, default=None): 1310 return super(CoercedDict, self).setdefault(key, 1311 self._coerce_item(key, 1312 default)) 1313 1314 1315class CoercedSet(CoercedCollectionMixin, set): 1316 """Set which coerces its values 1317 1318 Dict implementation which overrides all element-adding methods and 1319 coercing the element(s) being added to the required element type 1320 """ 1321 def _coerce_element(self, element): 1322 if hasattr(self, "_element_type") and self._element_type is not None: 1323 return self._element_type.coerce(self._obj, 1324 "%s[%s]" % (self._field, element), 1325 element) 1326 else: 1327 return element 1328 1329 def _coerce_iterable(self, values): 1330 coerced = set() 1331 for element in values: 1332 coerced.add(self._coerce_element(element)) 1333 return coerced 1334 1335 def add(self, value): 1336 return super(CoercedSet, self).add(self._coerce_element(value)) 1337 1338 def update(self, values): 1339 return super(CoercedSet, self).update(self._coerce_iterable(values)) 1340 1341 def symmetric_difference_update(self, values): 1342 return super(CoercedSet, self).symmetric_difference_update( 1343 self._coerce_iterable(values)) 1344 1345 def __ior__(self, y): 1346 return super(CoercedSet, self).__ior__(self._coerce_iterable(y)) 1347 1348 def __ixor__(self, y): 1349 return super(CoercedSet, self).__ixor__(self._coerce_iterable(y)) 1350