diff options
Diffstat (limited to 'netmodel/model')
-rw-r--r-- | netmodel/model/attribute.py | 120 | ||||
-rw-r--r-- | netmodel/model/collection.py | 5 | ||||
-rw-r--r-- | netmodel/model/key.py | 19 | ||||
-rw-r--r-- | netmodel/model/mapper.py | 4 | ||||
-rw-r--r-- | netmodel/model/object.py | 126 | ||||
-rw-r--r-- | netmodel/model/query.py | 13 | ||||
-rw-r--r-- | netmodel/model/sa_collections.py | 265 | ||||
-rw-r--r-- | netmodel/model/sa_compat.py | 270 | ||||
-rw-r--r-- | netmodel/model/type.py | 185 | ||||
-rw-r--r-- | netmodel/model/uuid.py | 51 |
10 files changed, 940 insertions, 118 deletions
diff --git a/netmodel/model/attribute.py b/netmodel/model/attribute.py index b69ee1bf..b2fa2331 100644 --- a/netmodel/model/attribute.py +++ b/netmodel/model/attribute.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # Copyright (c) 2017 Cisco and/or its affiliates. @@ -22,11 +22,10 @@ import logging import operator import types -from netmodel.model.mapper import ObjectSpecification -from netmodel.model.type import is_type -from netmodel.util.meta import inheritors -from netmodel.util.misc import is_iterable -from vicn.core.sa_collections import InstrumentedList, _list_decorators +from netmodel.model.mapper import ObjectSpecification +from netmodel.model.type import Type, Self +from netmodel.util.misc import is_iterable +from netmodel.model.collection import Collection log = logging.getLogger(__name__) instance_dict = operator.attrgetter('__dict__') @@ -38,26 +37,25 @@ class NEVER_SET: None #------------------------------------------------------------------------------ class Multiplicity: - _1_1 = '1_1' - _1_N = '1_N' - _N_1 = 'N_1' - _N_N = 'N_N' - + OneToOne = '1_1' + OneToMany = '1_N' + ManyToOne = 'N_1' + ManyToMany = 'N_N' @staticmethod def reverse(value): reverse_map = { - Multiplicity._1_1: Multiplicity._1_1, - Multiplicity._1_N: Multiplicity._N_1, - Multiplicity._N_1: Multiplicity._1_N, - Multiplicity._N_N: Multiplicity._N_N, + Multiplicity.OneToOne: Multiplicity.OneToOne, + Multiplicity.OneToMany: Multiplicity.ManyToOne, + Multiplicity.ManyToOne: Multiplicity.OneToMany, + Multiplicity.ManyToMany: Multiplicity.ManyToMany, } return reverse_map[value] # Default attribute properties values (default to None) DEFAULT = { - 'multiplicity' : Multiplicity._1_1, + 'multiplicity' : Multiplicity.OneToOne, 'mandatory' : False, } @@ -71,33 +69,36 @@ class Attribute(abc.ABC, ObjectSpecification): 'type', 'description', 'default', - 'choices', 'mandatory', 'multiplicity', 'ro', 'auto', 'func', - 'requirements', 'reverse_name', 'reverse_description', 'reverse_auto' ] def __init__(self, *args, **kwargs): - for key in Attribute.properties: + for key in kwargs.keys(): + if not key in self.properties: + raise ValueError("Invalid attribute property {}".format(key)) + for key in self.properties: value = kwargs.pop(key, NEVER_SET) setattr(self, key, value) if len(args) == 1: self.type, = args - elif len(args) == 2: - self.name, self.type = args - assert is_type(self.type) + # self.type is optional since the type can be inherited. Although we + # will have to verify the attribute is complete at some point + if isinstance(self.type, str): + self.type = Type.from_string(self.type) + assert self.type is Self or Type.exists(self.type) self.is_aggregate = False self._reverse_attributes = list() - + #-------------------------------------------------------------------------- # Display #-------------------------------------------------------------------------- @@ -107,6 +108,15 @@ class Attribute(abc.ABC, ObjectSpecification): __str__ = __repr__ + # The following functions are required to allow comparing attributes, and + # using them as dict keys + + def __eq__(self, other): + return self.name == other.name + + def __hash__(self): + return hash(self.name) + #-------------------------------------------------------------------------- # Descriptor protocol # @@ -118,7 +128,7 @@ class Attribute(abc.ABC, ObjectSpecification): return self value = instance_dict(instance).get(self.name, NEVER_SET) - + # Case : collection attribute if self.is_collection: if value is NEVER_SET: @@ -126,12 +136,12 @@ class Attribute(abc.ABC, ObjectSpecification): default = self.default(instance) else: default = self.default - value = InstrumentedList(default) + value = Collection(default) value._attribute = self value._instance = instance self.__set__(instance, value) return value - return value + return value # Case : scalar attribute @@ -159,8 +169,8 @@ class Attribute(abc.ABC, ObjectSpecification): return if self.is_collection: - if not isinstance(value, InstrumentedList): - value = InstrumentedList(value) + if not isinstance(value, Collection): + value = Collection(value) value._attribute = self value._instance = instance @@ -172,6 +182,10 @@ class Attribute(abc.ABC, ObjectSpecification): def __delete__(self, instance): raise NotImplementedError + def __set_name__(self, owner, name): + self.name = name + self.owner = owner + #-------------------------------------------------------------------------- # Accessors #-------------------------------------------------------------------------- @@ -184,17 +198,16 @@ class Attribute(abc.ABC, ObjectSpecification): return DEFAULT.get(name, None) return value - # Shortcuts - def has_reverse_attribute(self): return self.reverse_name and self.multiplicity @property def is_collection(self): - return self.multiplicity in (Multiplicity._1_N, Multiplicity._N_N) + return self.multiplicity in (Multiplicity.OneToMany, + Multiplicity.ManyToMany) def is_set(self, instance): - return self.name in instance_dict(instance) + return instance.is_set(self.name) #-------------------------------------------------------------------------- # Operations @@ -217,46 +230,3 @@ class Attribute(abc.ABC, ObjectSpecification): value.extend(parent_value) else: setattr(self, prop, parent_value) - - #-------------------------------------------------------------------------- - # Attribute values - #-------------------------------------------------------------------------- - - def _handle_getitem(self, instance, item): - return item - - def _handle_add(self, instance, item): - instance._state.dirty = True - instance._state.attr_dirty.add(self.name) - print('marking', self.name, 'as dirty') - return item - - def _handle_remove(self, instance, item): - instance._state.dirty = True - instance._state.attr_dirty.add(self.name) - print('marking', self.name, 'as dirty') - - def _handle_before_remove(self, instance): - pass - - #-------------------------------------------------------------------------- - # Attribute values - #-------------------------------------------------------------------------- - -class Relation(Attribute): - properties = Attribute.properties[:] - properties.extend([ - 'reverse_name', - 'reverse_description', - 'multiplicity', - ]) - -class SelfRelation(Relation): - def __init__(self, *args, **kwargs): - if args: - if not len(args) == 1: - raise ValueError('Bad initialized for SelfRelation') - name, = args - super().__init__(name, None, *args, **kwargs) - else: - super().__init__(None, *args, **kwargs) diff --git a/netmodel/model/collection.py b/netmodel/model/collection.py index 21be84d8..01a63299 100644 --- a/netmodel/model/collection.py +++ b/netmodel/model/collection.py @@ -16,9 +16,10 @@ # limitations under the License. # -from netmodel.model.filter import Filter +from netmodel.model.sa_collections import InstrumentedList +from netmodel.model.filter import Filter -class Collection(list): +class Collection(InstrumentedList): """ A collection corresponds to a list of objects, and includes processing functionalities to manipulate them. diff --git a/netmodel/model/key.py b/netmodel/model/key.py new file mode 100644 index 00000000..bc49af03 --- /dev/null +++ b/netmodel/model/key.py @@ -0,0 +1,19 @@ +from netmodel.model.mapper import ObjectSpecification + +class Key(ObjectSpecification): + def __init__(self, *attributes): + self._attributes = attributes + + #-------------------------------------------------------------------------- + # Descriptor protocol + # + # see. https://docs.python.org/3/howto/descriptor.html + #-------------------------------------------------------------------------- + + def __set_name__(self, owner, name): + self._name = name + self._owner = owner + + def __iter__(self): + for attribute in self._attributes: + yield attribute diff --git a/netmodel/model/mapper.py b/netmodel/model/mapper.py index 9be46a14..856238c7 100644 --- a/netmodel/model/mapper.py +++ b/netmodel/model/mapper.py @@ -16,5 +16,7 @@ # limitations under the License. # -class ObjectSpecification: +import sys + +class ObjectSpecification: pass diff --git a/netmodel/model/object.py b/netmodel/model/object.py index 32d3a833..99dbe0c2 100644 --- a/netmodel/model/object.py +++ b/netmodel/model/object.py @@ -18,7 +18,10 @@ from abc import ABCMeta -from netmodel.model.attribute import Attribute +import sys + +from netmodel.model.attribute import Attribute, Multiplicity +from netmodel.model.key import Key from netmodel.model.type import BaseType from netmodel.model.mapper import ObjectSpecification @@ -26,11 +29,21 @@ from netmodel.model.mapper import ObjectSpecification E_UNK_RES_NAME = 'Unknown resource name for attribute {} in {} ({}) : {}' -class ObjectMetaclass(ABCMeta): +class ObjectMetaclass(type): """ Object metaclass allowing non-uniform attribute declaration. """ + def __new__(mcls, name, bases, attrs): + cls = super(ObjectMetaclass, mcls).__new__(mcls, name, bases, attrs) + if (sys.version_info < (3, 6)): + # Before Python 3.6, descriptor protocol does not include __set_name__. + # We use a metaclass to emulate the functionality. + for attr, obj in attrs.items(): + if isinstance(obj, ObjectSpecification): + obj.__set_name__(cls, attr) + return cls + def __init__(cls, class_name, parents, attrs): """ Args: @@ -67,19 +80,19 @@ class Object(BaseType, metaclass = ObjectMetaclass): else: resource = x if not resource: - raise LurchException(E_UNK_RES_NAME.format(key, + raise LurchException(E_UNK_RES_NAME.format(key, self.name, self.__class__.__name__, x)) new_value.append(resource._state.uuid) value = new_value else: if isinstance(value, str): - resource = self._state.manager.by_name(value) + resource = self._state.manager.by_name(value) elif isinstance(value, UUID): - resource = self._state.manager.by_uuid(value) + resource = self._state.manager.by_uuid(value) else: resource = value if not resource: - raise LurchException(E_UNK_RES_NAME.format(key, + raise LurchException(E_UNK_RES_NAME.format(key, self.name, self.__class__.__name__, value)) value = resource._state.uuid setattr(self, key, value) @@ -111,19 +124,20 @@ class Object(BaseType, metaclass = ObjectMetaclass): @classmethod def _sanitize(cls): - """Sanitize the object model to accomodate for multiple declaration - styles + """ + This methods performs sanitization of the object declaration. + + More specifically: + - it goes over all attributes and sets their name based on the python + object attribute name. + - it establishes mutual object relationships through reverse attributes. - In particular, this method: - - set names to all attributes """ cls._reverse_attributes = dict() cur_reverse_attributes = dict() for name, obj in vars(cls).items(): - if not isinstance(obj, ObjectSpecification): + if not isinstance(obj, Attribute): continue - if isinstance(obj, Attribute): - obj.name = name # Remember whether a reverse_name is defined before loading # inherited properties from parent @@ -135,30 +149,72 @@ class Object(BaseType, metaclass = ObjectMetaclass): if hasattr(base, name): parent_attribute = getattr(base, name) obj.merge(parent_attribute) - assert obj.type + assert obj.type, "No type for obj={} cls={}, base={}".format(obj, cls, base) # Handle reverse attribute # # NOTE: we need to do this after merging to be sure we get all # properties inherited from parent (eg. multiplicity) + # + # See "Reverse attributes" section in BaseResource docstring. + # + # Continueing with the same example, let's detail how it is handled: + # + # Original declaration: + # >>> + # class Group(Resource): + # resources = Attribute(Resource, description = 'Resources belonging to the group', + # multiplicity = Multiplicity.ManyToMany, + # default = [], + # reverse_name = 'groups', + # reverse_description = 'Groups to which the resource belongs') + # <<< + # + # Local variables: + # cls = <class 'vicn.resource.group.Group'> + # obj = <Attribute resources> + # obj.type = <class 'vicn.core.Resource'> + # reverse_attribute = <Attribute groups> + # + # Result: + # 1) Group._reverse_attributes = + # { <Attribute resources> : [<Attribute groups>, ...], ...} + # 2) Add attribute <Attribute groups> to class Resource + # 3) Resource._reverse_attributes = + # { <Attribute groups> : [<Attribute resources], ...], ...} + # if has_reverse: a = { - 'name' : obj.reverse_name, - 'description' : obj.reverse_description, - 'multiplicity' : Multiplicity.reverse(obj.multiplicity), - 'auto' : obj.reverse_auto, + 'name' : obj.reverse_name, + 'description' : obj.reverse_description, + 'multiplicity' : Multiplicity.reverse(obj.multiplicity), + 'reverse_name' : obj.name, + 'reverse_description' : obj.description, + 'auto' : obj.reverse_auto, } - reverse_attribute = Attribute(cls, **a) + + # We need to use the same class as the Attribute ! + reverse_attribute = obj.__class__(cls, **a) reverse_attribute.is_aggregate = True + # 1) Store the reverse attributes to be later inserted in the + # remote class, at the end of the function + # TODO : clarify the reasons to perform this in two steps cur_reverse_attributes[obj.type] = reverse_attribute + # 2) if not obj in cls._reverse_attributes: cls._reverse_attributes[obj] = list() cls._reverse_attributes[obj].append(reverse_attribute) - for cls, a in cur_reverse_attributes.items(): - setattr(cls, a.name, a) + # 3) + if not reverse_attribute in obj.type._reverse_attributes: + obj.type._reverse_attributes[reverse_attribute] = list() + obj.type._reverse_attributes[reverse_attribute].append(obj) + + # Insert newly created reverse attributes in the remote class + for kls, a in cur_reverse_attributes.items(): + setattr(kls, a.name, a) @classmethod def iter_attributes(cls, aggregates = False): @@ -168,7 +224,7 @@ class Object(BaseType, metaclass = ObjectMetaclass): continue if attribute.is_aggregate and not aggregates: continue - + yield attribute def get_attributes(self, aggregates = False): @@ -178,7 +234,7 @@ class Object(BaseType, metaclass = ObjectMetaclass): return set(a.name for a in self.iter_attributes(aggregates = \ aggregates)) - def get_attribute_dict(self, field_names = None, aggregates = False, + def get_attribute_dict(self, field_names = None, aggregates = False, uuid = True): assert not field_names or field_names.is_star() attributes = self.get_attributes(aggregates = aggregates) @@ -192,14 +248,29 @@ class Object(BaseType, metaclass = ObjectMetaclass): ret[a.name] = list() for x in value: if uuid and isinstance(x, Object): - x = x._state.uuid._uuid + x = x._state.uuid._uuid ret[a.name].append(x) else: if uuid and isinstance(value, Object): - value = value._state.uuid._uuid + value = value._state.uuid._uuid ret[a.name] = value return ret + @classmethod + def iter_keys(cls): + for name in dir(cls): + key = getattr(cls, name) + if not isinstance(key, Key): + continue + yield key + + @classmethod + def get_keys(cls): + return list(cls.iter_keys()) + + def get_key_dicts(self): + return [{attribute: self.get(attribute.name) for attribute in key} for key in self.iter_keys()] + def get_tuple(self): return (self.__class__, self._get_attribute_dict()) @@ -229,3 +300,8 @@ class Object(BaseType, metaclass = ObjectMetaclass): def has_attribute(cls, name): return name in [a.name for a in cls.attributes()] + def get(self, attribute_name): + raise NotImplementedError + + def set(self, attribute_name, value): + raise NotImplementedError diff --git a/netmodel/model/query.py b/netmodel/model/query.py index c182cb45..a1d331fb 100644 --- a/netmodel/model/query.py +++ b/netmodel/model/query.py @@ -46,7 +46,7 @@ FUNCTION2STR = { STR2FUNCTION = dict((v, k) for k, v in FUNCTION2STR.items()) class Query: - def __init__(self, action, object_name, filter = None, params = None, + def __init__(self, action, object_name, filter = None, params = None, field_names = None, aggregate = None, last = False, reply = False): self.action = action self.object_name = object_name @@ -64,13 +64,13 @@ class Query: if field_names: if isinstance(field_names, FieldNames): self.field_names = field_names - else: + else: self.field_names = FieldNames(field_names) else: self.field_names = FieldNames() self.aggregate = aggregate - + self.last = last self.reply = reply @@ -100,7 +100,7 @@ class Query: field_names = FieldNames(star = True) last = dic.get('last', False) reply = dic.get('reply', False) - return Query(action, object_name, filter, params, field_names, + return Query(action, object_name, filter, params, field_names, aggregate, last) def to_sql(self, multiline = False): @@ -140,8 +140,7 @@ class Query: return strmap[self.action] % locals() - def __str__(self): - return self.to_sql() - def __repr__(self): return self.to_sql() + + __str__ = __repr__ diff --git a/netmodel/model/sa_collections.py b/netmodel/model/sa_collections.py new file mode 100644 index 00000000..5e651061 --- /dev/null +++ b/netmodel/model/sa_collections.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# This module is derived from code from SQLAlchemy +# +# orm/collections.py +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# + +import logging + +from netmodel.model.sa_compat import py2k +from netmodel.model.uuid import UUID + +class InstrumentedListException(Exception): pass + +log = logging.getLogger(__name__) + +def _list_decorators(): + """Tailored instrumentation wrappers for any list-like class.""" + + def _tidy(fn): + fn._sa_instrumented = True + fn.__doc__ = getattr(list, fn.__name__).__doc__ + + def append(fn): + def append(self, item): + try: + item = self._attribute.do_list_add(self._instance, item) + fn(self, item) + except InstrumentedListException as e: + pass + _tidy(append) + return append + + def remove(fn): + def remove(self, value): + # testlib.pragma exempt:__eq__ + try: + self._attribute.do_list_remove(self._instance, value) + fn(self, value) + except : pass + _tidy(remove) + return remove + + def insert(fn): + def insert(self, index, value): + try: + value = self._attribute.do_list_add(self._instance, item) + fn(self, index, value) + except : pass + _tidy(insert) + return insert + + def __getitem__(fn): + def __getitem__(self, index): + item = fn(self, index) + return self._attribute.handle_getitem(self._instance, item) + _tidy(__getitem__) + return __getitem__ + + def __setitem__(fn): + def __setitem__(self, index, value): + if not isinstance(index, slice): + existing = self[index] + if existing is not None: + try: + self._attribute.do_list_remove(self._instance, existing) + except: pass + try: + value = self._attribute.do_list_add(self._instance, value) + fn(self, index, value) + except: pass + else: + # slice assignment requires __delitem__, insert, __len__ + step = index.step or 1 + start = index.start or 0 + if start < 0: + start += len(self) + if index.stop is not None: + stop = index.stop + else: + stop = len(self) + if stop < 0: + stop += len(self) + + if step == 1: + for i in range(start, stop, step): + if len(self) > start: + del self[start] + + for i, item in enumerate(value): + self.insert(i + start, item) + else: + rng = list(range(start, stop, step)) + if len(value) != len(rng): + raise ValueError( + "attempt to assign sequence of size %s to " + "extended slice of size %s" % (len(value), + len(rng))) + for i, item in zip(rng, value): + self.__setitem__(i, item) + _tidy(__setitem__) + return __setitem__ + + def __delitem__(fn): + def __delitem__(self, index): + if not isinstance(index, slice): + item = self[index] + try: + self._attribute.do_list_remove(self._instance, item) + fn(self, index) + except : pass + else: + # slice deletion requires __getslice__ and a slice-groking + # __getitem__ for stepped deletion + # note: not breaking this into atomic dels + has_except = False + for item in self[index]: + try: + self._attribute.do_list_remove(self._instance, item) + except : has_except = True + if not has_except: + fn(self, index) + _tidy(__delitem__) + return __delitem__ + + if py2k: + def __setslice__(fn): + def __setslice__(self, start, end, values): + has_except = False + for value in self[start:end]: + try: + self._attribute.do_list_remove(self._instance, value) + except : has_except = True + #values = [self._attribute.do_list_add(self._instance, value) for value in values] + _values = list() + for value in values: + try: + _values.append(self._attribute.do_list_add(self._instance, value)) + except: has_except = True + if not has_except: + fn(self, start, end, _values) + _tidy(__setslice__) + return __setslice__ + + def __delslice__(fn): + def __delslice__(self, start, end): + has_except = False + for value in self[start:end]: + try: + self._attribute.do_list_remove(self._instance, value) + except : has_except = True + if not has_except: + fn(self, start, end) + _tidy(__delslice__) + return __delslice__ + + def extend(fn): + def extend(self, iterable): + for value in iterable: + self.append(value) + _tidy(extend) + return extend + + def __iadd__(fn): + def __iadd__(self, iterable): + # list.__iadd__ takes any iterable and seems to let TypeError + # raise as-is instead of returning NotImplemented + for value in iterable: + self.append(value) + return self + _tidy(__iadd__) + return __iadd__ + + def pop(fn): + def pop(self, index=-1): + try: + self._attribute.do_list_remove(self._instance, item) + item = fn(self, index) + return item + except : return None + _tidy(pop) + return pop + + def __iter__(fn): + def __iter__(self): + for item in fn(self): + yield self._attribute.handle_getitem(self._instance, item) + _tidy(__iter__) + return __iter__ + + def __repr__(fn): + def __repr__(self): + return '<Collection {} {}>'.format(id(self), list.__repr__(self)) + _tidy(__repr__) + return __repr__ + + __str__ = __repr__ + #def __str__(fn): + # def __str__(self): + # return str(list(self)) + # _tidy(__str__) + # return __str__ + + if not py2k: + def clear(fn): + def clear(self, index=-1): + has_except = False + for item in self: + try: + self._attribute.do_list_remove(self._instance, item) + except : has_except = True + if not has_except: + fn(self) + _tidy(clear) + return clear + + # __imul__ : not wrapping this. all members of the collection are already + # present, so no need to fire appends... wrapping it with an explicit + # decorator is still possible, so events on *= can be had if they're + # desired. hard to imagine a use case for __imul__, though. + + l = locals().copy() + l.pop('_tidy') + return l + +def _instrument_list(cls): + # inspired by sqlalchemy + for method, decorator in _list_decorators().items(): + fn = getattr(cls, method, None) + if fn: + #if (fn and method not in methods and + # not hasattr(fn, '_sa_instrumented')): + setattr(cls, method, decorator(fn)) + +class InstrumentedList(list): + + @classmethod + def from_list(cls, value, instance, attribute): + lst = list() + if value: + for x in value: + if isinstance(x, UUID): + x = instance.from_uuid(x) + lst.append(x) + # Having a class method is important for inheritance + value = cls(lst) + value._attribute = attribute + value._instance = instance + return value + + def __contains__(self, key): + from vicn.core.resource import Resource + if isinstance(key, Resource): + key = key.get_uuid() + return list.__contains__(self, key) + + def __lshift__(self, item): + self.append(item) + +_instrument_list(InstrumentedList) diff --git a/netmodel/model/sa_compat.py b/netmodel/model/sa_compat.py new file mode 100644 index 00000000..34211455 --- /dev/null +++ b/netmodel/model/sa_compat.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# This module originates from SQLAlchemy +# +# util/compat.py +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# + +"""Handle Python version/platform incompatibilities.""" + +import sys + +try: + import threading +except ImportError: + import dummy_threading as threading + +py36 = sys.version_info >= (3, 6) +py33 = sys.version_info >= (3, 3) +py32 = sys.version_info >= (3, 2) +py3k = sys.version_info >= (3, 0) +py2k = sys.version_info < (3, 0) +py265 = sys.version_info >= (2, 6, 5) +jython = sys.platform.startswith('java') +pypy = hasattr(sys, 'pypy_version_info') +win32 = sys.platform.startswith('win') +cpython = not pypy and not jython # TODO: something better for this ? + +import collections +next = next + +if py3k: + import pickle +else: + try: + import cPickle as pickle + except ImportError: + import pickle + +# work around http://bugs.python.org/issue2646 +if py265: + safe_kwarg = lambda arg: arg +else: + safe_kwarg = str + +ArgSpec = collections.namedtuple("ArgSpec", + ["args", "varargs", "keywords", "defaults"]) + +if py3k: + import builtins + + from inspect import getfullargspec as inspect_getfullargspec + from urllib.parse import (quote_plus, unquote_plus, + parse_qsl, quote, unquote) + import configparser + from io import StringIO + + from io import BytesIO as byte_buffer + + def inspect_getargspec(func): + return ArgSpec( + *inspect_getfullargspec(func)[0:4] + ) + + string_types = str, + binary_types = bytes, + binary_type = bytes + text_type = str + int_types = int, + iterbytes = iter + + def u(s): + return s + + def ue(s): + return s + + def b(s): + return s.encode("latin-1") + + if py32: + callable = callable + else: + def callable(fn): + return hasattr(fn, '__call__') + + def cmp(a, b): + return (a > b) - (a < b) + + from functools import reduce + + print_ = getattr(builtins, "print") + + import_ = getattr(builtins, '__import__') + + import itertools + itertools_filterfalse = itertools.filterfalse + itertools_filter = filter + itertools_imap = map + from itertools import zip_longest + + import base64 + + def b64encode(x): + return base64.b64encode(x).decode('ascii') + + def b64decode(x): + return base64.b64decode(x.encode('ascii')) + +else: + from inspect import getargspec as inspect_getfullargspec + inspect_getargspec = inspect_getfullargspec + from urllib import quote_plus, unquote_plus, quote, unquote + from urlparse import parse_qsl + import ConfigParser as configparser + from StringIO import StringIO + from cStringIO import StringIO as byte_buffer + + string_types = basestring, + binary_types = bytes, + binary_type = str + text_type = unicode + int_types = int, long + + def iterbytes(buf): + return (ord(byte) for byte in buf) + + def u(s): + # this differs from what six does, which doesn't support non-ASCII + # strings - we only use u() with + # literal source strings, and all our source files with non-ascii + # in them (all are tests) are utf-8 encoded. + return unicode(s, "utf-8") + + def ue(s): + return unicode(s, "unicode_escape") + + def b(s): + return s + + def import_(*args): + if len(args) == 4: + args = args[0:3] + ([str(arg) for arg in args[3]],) + return __import__(*args) + + callable = callable + cmp = cmp + reduce = reduce + + import base64 + b64encode = base64.b64encode + b64decode = base64.b64decode + + def print_(*args, **kwargs): + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + for arg in enumerate(args): + if not isinstance(arg, basestring): + arg = str(arg) + fp.write(arg) + + import itertools + itertools_filterfalse = itertools.ifilterfalse + itertools_filter = itertools.ifilter + itertools_imap = itertools.imap + from itertools import izip_longest as zip_longest + + +import time +if win32 or jython: + time_func = time.clock +else: + time_func = time.time + +from collections import namedtuple +from operator import attrgetter as dottedgetter + + +if py3k: + def reraise(tp, value, tb=None, cause=None): + if cause is not None: + assert cause is not value, "Same cause emitted" + value.__cause__ = cause + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + +else: + # not as nice as that of Py3K, but at least preserves + # the code line where the issue occurred + exec("def reraise(tp, value, tb=None, cause=None):\n" + " if cause is not None:\n" + " assert cause is not value, 'Same cause emitted'\n" + " raise tp, value, tb\n") + + +def raise_from_cause(exception, exc_info=None): + if exc_info is None: + exc_info = sys.exc_info() + exc_type, exc_value, exc_tb = exc_info + cause = exc_value if exc_value is not exception else None + reraise(type(exception), exception, tb=exc_tb, cause=cause) + +if py3k: + exec_ = getattr(builtins, 'exec') +else: + def exec_(func_text, globals_, lcl=None): + if lcl is None: + exec('exec func_text in globals_') + else: + exec('exec func_text in globals_, lcl') + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass. + + Drops the middle class upon creation. + + Source: http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/ + + """ + + class metaclass(meta): + __call__ = type.__call__ + __init__ = type.__init__ + + def __new__(cls, name, this_bases, d): + if this_bases is None: + return type.__new__(cls, name, (), d) + return meta(name, bases, d) + return metaclass('temporary_class', None, {}) + + +from contextlib import contextmanager + +try: + from contextlib import nested +except ImportError: + # removed in py3k, credit to mitsuhiko for + # workaround + + @contextmanager + def nested(*managers): + exits = [] + vars = [] + exc = (None, None, None) + try: + for mgr in managers: + exit = mgr.__exit__ + enter = mgr.__enter__ + vars.append(enter()) + exits.append(exit) + yield vars + except: + exc = sys.exc_info() + finally: + while exits: + exit = exits.pop() + try: + if exit(*exc): + exc = (None, None, None) + except: + exc = sys.exc_info() + if exc != (None, None, None): + reraise(exc[0], exc[1], exc[2]) diff --git a/netmodel/model/type.py b/netmodel/model/type.py index 20dc2580..9a7b8740 100644 --- a/netmodel/model/type.py +++ b/netmodel/model/type.py @@ -16,27 +16,47 @@ # limitations under the License. # +from socket import inet_pton, inet_ntop, AF_INET6 +from struct import unpack, pack +from abc import ABCMeta + from netmodel.util.meta import inheritors class BaseType: + __choices__ = None + @staticmethod def name(): return self.__class__.__name__.lower() + @classmethod + def restrict(cls, **kwargs): + class BaseType(cls): + __choices__ = kwargs.pop('choices', None) + return BaseType + class String(BaseType): - def __init__(self, *args, **kwargs): - self._min_size = kwargs.pop('min_size', None) - self._max_size = kwargs.pop('max_size', None) - self._ascii = kwargs.pop('ascii', False) - self._forbidden = kwargs.pop('forbidden', None) - super().__init__() + __min_size__ = None + __max_size__ = None + __ascii__ = None + __forbidden__ = None + + @classmethod + def restrict(cls, **kwargs): + base = super().restrict(**kwargs) + class String(base): + __max_size__ = kwargs.pop('max_size', None) + __min_size__ = kwargs.pop('min_size', None) + __ascii__ = kwargs.pop('ascii', None) + __forbidden__ = kwargs.pop('forbidden', None) + return String class Integer(BaseType): def __init__(self, *args, **kwargs): self._min_value = kwargs.pop('min_value', None) self._max_value = kwargs.pop('max_value', None) super().__init__() - + class Double(BaseType): def __init__(self, *args, **kwargs): self._min_value = kwargs.pop('min_value', None) @@ -49,12 +69,161 @@ class Bool(BaseType): class Dict(BaseType): pass +class PrefixTreeException(Exception): pass +class NotEnoughAddresses(PrefixTreeException): pass +class UnassignablePrefix(PrefixTreeException): pass + +class Prefix(BaseType, metaclass=ABCMeta): + + def __init__(self, ip_address, prefix_len=None): + if not prefix_len: + if not isinstance(ip_address, str): + import pdb; pdb.set_trace() + if '/' in ip_address: + ip_address, prefix_len = ip_address.split('/') + prefix_len = int(prefix_len) + else: + prefix_len = self.MAX_PREFIX_SIZE + if isinstance(ip_address, str): + ip_address = self.aton(ip_address) + self.ip_address = ip_address + self.prefix_len = prefix_len + + def __contains__(self, obj): + #it can be an IP as a integer + if isinstance(obj, int): + obj = type(self)(obj, self.MAX_PREFIX_SIZE) + #Or it's an IP string + if isinstance(obj, str): + #It's a prefix as 'IP/prefix' + if '/' in obj: + split_obj = obj.split('/') + obj = type(self)(split_obj[0], int(split_obj[1])) + else: + obj = type(self)(obj, self.MAX_PREFIX_SIZE) + + return self._contains_prefix(obj) + + @classmethod + def mask(cls): + mask_len = cls.MAX_PREFIX_SIZE//8 #Converts from bits to bytes + mask = 0 + for step in range(0,mask_len): + mask = (mask << 8) | 0xff + return mask + + def _contains_prefix(self, prefix): + assert isinstance(prefix, type(self)) + return (prefix.prefix_len >= self.prefix_len and + prefix.ip_address >= self.first_prefix_address() and + prefix.ip_address <= self.last_prefix_address()) + + #Returns the first address of a prefix + def first_prefix_address(self): + return self.ip_address & (self.mask() << (self.MAX_PREFIX_SIZE-self.prefix_len)) + + def canonical_prefix(self): + return type(self)(self.first_prefix_address(), self.prefix_len) + + def last_prefix_address(self): + return self.ip_address | (self.mask() >> self.prefix_len) + + def limits(self): + return self.first_prefix_address(), self.last_prefix_address() + + def __str__(self): + return "{}/{}".format(self.ntoa(self.first_prefix_address()), self.prefix_len) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + return self.get_tuple() == other.get_tuple() + + def get_tuple(self): + return (self.first_prefix_address(), self.prefix_len) + + def __hash__(self): + return hash(self.get_tuple()) + + def __iter__(self): + return self.get_iterator() + + #Iterates by steps of prefix_len, e.g., on all available /31 in a /24 + def get_iterator(self, prefix_len=None): + if prefix_len is None: + prefix_len=self.MAX_PREFIX_SIZE + assert (prefix_len >= self.prefix_len and prefix_len<=self.MAX_PREFIX_SIZE) + step = 2**(self.MAX_PREFIX_SIZE - prefix_len) + for ip in range(self.first_prefix_address(), self.last_prefix_address()+1, step): + yield type(self)(ip, prefix_len) + +class Inet4Prefix(Prefix): + + MAX_PREFIX_SIZE = 32 + + @classmethod + def aton(cls, address): + ret = 0 + components = address.split('.') + for comp in components: + ret = (ret << 8) + int(comp) + return ret + + @classmethod + def ntoa(cls, address): + components = [] + for _ in range(0,4): + components.insert(0,'{}'.format(address % 256)) + address = address >> 8 + return '.'.join(components) + +class Inet6Prefix(Prefix): + + MAX_PREFIX_SIZE = 128 + + @classmethod + def aton (cls, address): + prefix, suffix = unpack(">QQ", inet_pton(AF_INET6, address)) + return (prefix << 64) | suffix + + @classmethod + def ntoa (cls, address): + return inet_ntop(AF_INET6, pack(">QQ", address >> 64, address & ((1 << 64) -1))) + + #skip_internet_address: skip a:b::0, as v6 often use default /64 prefixes + def get_iterator(self, prefix_len=None, skip_internet_address=None): + if skip_internet_address is None: + #We skip the internet address if we iterate over Addresses + if prefix_len is None: + skip_internet_address = True + #But not if we iterate over prefixes + else: + skip_internet_address = False + it = super().get_iterator(prefix_len) + if skip_internet_address: + next(it) + return it + +class InetAddress(Prefix): + + def get_tuple(self): + return (self.ip_address, self.prefix_len) + + def __str__(self): + return self.ntoa(self.ip_address) + +class Inet4Address(InetAddress, Inet4Prefix): + pass + +class Inet6Address(InetAddress, Inet6Prefix): + pass + class Self(BaseType): """Self-reference """ class Type: - BASE_TYPES = (String, Integer, Double, Bool) + BASE_TYPES = (String, Integer, Double, Bool) _registry = dict() @staticmethod diff --git a/netmodel/model/uuid.py b/netmodel/model/uuid.py new file mode 100644 index 00000000..dae51d75 --- /dev/null +++ b/netmodel/model/uuid.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import random +import string + +# Separator for components of the UUID +UUID_SEP = '-' + +# Length of the random component of the UUID +UUID_LEN = 5 + +class UUID: + def __init__(self, name, cls): + self._uuid = self._make_uuid(name, cls) + + def _make_uuid(self, name, cls): + """Generate a unique resource identifier + + The UUID consists in the type of the resource, to which is added a + random identifier of length UUID_LEN. Components of the UUID are + separated by UUID_SEP. + """ + uuid = ''.join(random.choice(string.ascii_uppercase + string.digits) + for _ in range(UUID_LEN)) + if name: + uuid = name # + UUID_SEP + uuid + return UUID_SEP.join([cls.__name__, uuid]) + + def __repr__(self): + return '<UUID {}>'.format(self._uuid) + + def __lt__(self, other): + return self._uuid < other._uuid + + __str__ = __repr__ |