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