diff --git a/nimble/common/exception.py b/nimble/common/exception.py index bd491a8f..acca8bd6 100644 --- a/nimble/common/exception.py +++ b/nimble/common/exception.py @@ -118,3 +118,7 @@ class NotAcceptable(NimbleException): class ConfigInvalid(NimbleException): _msg_fmt = _("Invalid configuration file. %(error_msg)s") + + +class InvalidMAC(Invalid): + _msg_fmt = _("Expected a MAC address but received %(mac)s.") diff --git a/nimble/common/utils.py b/nimble/common/utils.py index 0b5a605a..209efe03 100644 --- a/nimble/common/utils.py +++ b/nimble/common/utils.py @@ -15,9 +15,12 @@ """Utilities and helper functions.""" +import re + from oslo_log import log as logging import six +from nimble.common import exception from nimble.common.i18n import _LW LOG = logging.getLogger(__name__) @@ -38,3 +41,34 @@ def safe_rstrip(value, chars=None): return value return value.rstrip(chars) or value + + +def is_valid_mac(address): + """Verify the format of a MAC address. + + Check if a MAC address is valid and contains six octets. Accepts + colon-separated format only. + + :param address: MAC address to be validated. + :returns: True if valid. False if not. + + """ + m = "[0-9a-f]{2}(:[0-9a-f]{2}){5}$" + return (isinstance(address, six.string_types) and + re.match(m, address.lower())) + + +def validate_and_normalize_mac(address): + """Validate a MAC address and return normalized form. + + Checks whether the supplied MAC address is formally correct and + normalize it to all lower case. + + :param address: MAC address to be validated and normalized. + :returns: Normalized and validated MAC address. + :raises: InvalidMAC If the MAC address is not valid. + + """ + if not is_valid_mac(address): + raise exception.InvalidMAC(mac=address) + return address.lower() diff --git a/nimble/objects/__init__.py b/nimble/objects/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nimble/objects/base.py b/nimble/objects/base.py new file mode 100644 index 00000000..67b01add --- /dev/null +++ b/nimble/objects/base.py @@ -0,0 +1,96 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# 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. + +"""Nimble common internal object model""" + +from oslo_utils import versionutils +from oslo_versionedobjects import base as object_base + +from nimble import objects +from nimble.objects import fields as object_fields + + +class NimbleObjectRegistry(object_base.VersionedObjectRegistry): + def registration_hook(self, cls, index): + # NOTE(jroll): blatantly stolen from nova + # NOTE(danms): This is called when an object is registered, + # and is responsible for maintaining nimble.objects.$OBJECT + # as the highest-versioned implementation of a given object. + version = versionutils.convert_version_to_tuple(cls.VERSION) + if not hasattr(objects, cls.obj_name()): + setattr(objects, cls.obj_name(), cls) + else: + cur_version = versionutils.convert_version_to_tuple( + getattr(objects, cls.obj_name()).VERSION) + if version >= cur_version: + setattr(objects, cls.obj_name(), cls) + + +class NimbleObject(object_base.VersionedObject): + """Base class and object factory. + + This forms the base of all objects that can be remoted or instantiated + via RPC. Simply defining a class that inherits from this base class + will make it remotely instantiatable. Objects should implement the + necessary "get" classmethod routines as well as "save" object methods + as appropriate. + """ + + OBJ_SERIAL_NAMESPACE = 'nimble_object' + OBJ_PROJECT_NAMESPACE = 'nimble' + + # TODO(lintan) Refactor these fields and create PersistentObject and + # TimeStampObject like Nova when it is necessary. + fields = { + 'created_at': object_fields.DateTimeField(nullable=True), + 'updated_at': object_fields.DateTimeField(nullable=True), + } + + def as_dict(self): + return dict((k, getattr(self, k)) + for k in self.fields + if hasattr(self, k)) + + def obj_refresh(self, loaded_object): + """Applies updates for objects that inherit from base.NimbleObject. + + Checks for updated attributes in an object. Updates are applied from + the loaded object column by column in comparison with the current + object. + """ + for field in self.fields: + if (self.obj_attr_is_set(field) and + self[field] != loaded_object[field]): + self[field] = loaded_object[field] + + @staticmethod + def _from_db_object(obj, db_object): + """Converts a database entity to a formal object. + + :param obj: An object of the class. + :param db_object: A DB model of the object + :return: The object of the class with the database entity added + """ + + for field in obj.fields: + obj[field] = db_object[field] + + obj.obj_reset_changes() + return obj + + +class NimbleObjectSerializer(object_base.VersionedObjectSerializer): + # Base class to use for object hydration + OBJ_BASE_CLASS = NimbleObject diff --git a/nimble/objects/fields.py b/nimble/objects/fields.py new file mode 100644 index 00000000..7f368318 --- /dev/null +++ b/nimble/objects/fields.py @@ -0,0 +1,108 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# 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 ast +import hashlib +import inspect +import six + +from oslo_versionedobjects import fields as object_fields + +from nimble.common import utils + + +class IntegerField(object_fields.IntegerField): + pass + + +class UUIDField(object_fields.UUIDField): + pass + + +class StringField(object_fields.StringField): + pass + + +class StringAcceptsCallable(object_fields.String): + @staticmethod + def coerce(obj, attr, value): + if callable(value): + value = value() + return super(StringAcceptsCallable, StringAcceptsCallable).coerce( + obj, attr, value) + + +class StringFieldThatAcceptsCallable(object_fields.StringField): + """Custom StringField object that allows for functions as default + + In some cases we need to allow for dynamic defaults based on configuration + options, this StringField object allows for a function to be passed as a + default, and will only process it at the point the field is coerced + """ + + AUTO_TYPE = StringAcceptsCallable() + + def __repr__(self): + default = self._default + if (self._default != object_fields.UnspecifiedDefault and + callable(self._default)): + default = "%s-%s" % ( + self._default.__name__, + hashlib.md5(inspect.getsource( + self._default).encode()).hexdigest()) + return '%s(default=%s,nullable=%s)' % (self._type.__class__.__name__, + default, self._nullable) + + +class DateTimeField(object_fields.DateTimeField): + pass + + +class BooleanField(object_fields.BooleanField): + pass + + +class ListOfStringsField(object_fields.ListOfStringsField): + pass + + +class FlexibleDict(object_fields.FieldType): + @staticmethod + def coerce(obj, attr, value): + if isinstance(value, six.string_types): + value = ast.literal_eval(value) + return dict(value) + + +class FlexibleDictField(object_fields.AutoTypedField): + AUTO_TYPE = FlexibleDict() + + # TODO(lucasagomes): In our code we've always translated None to {}, + # this method makes this field to work like this. But probably won't + # be accepted as-is in the oslo_versionedobjects library + def _null(self, obj, attr): + if self.nullable: + return {} + super(FlexibleDictField, self)._null(obj, attr) + + +class MACAddress(object_fields.FieldType): + @staticmethod + def coerce(obj, attr, value): + return utils.validate_and_normalize_mac(value) + + +class MACAddressField(object_fields.AutoTypedField): + AUTO_TYPE = MACAddress()