From 9ad6b58e37f8433c93b56f2d2d21cf0a7af719fe Mon Sep 17 00:00:00 2001 From: vinod pandarinathan Date: Sat, 6 Jun 2015 18:29:46 -0700 Subject: [PATCH] Versioned objects for cloudpulse Implements: blueprint versioned-objects Change-Id: I3747f4ab99985c1310aa5a301a04382408d48d58 --- cloudpulse/common/__init__.py | 19 +++ cloudpulse/objects/__init__.py | 19 +++ cloudpulse/objects/base.py | 108 +++++++++++++++++ cloudpulse/objects/cpulse.py | 205 +++++++++++++++++++++++++++++++++ cloudpulse/objects/fields.py | 19 +++ cloudpulse/objects/utils.py | 134 +++++++++++++++++++++ requirements.txt | 4 + 7 files changed, 508 insertions(+) create mode 100644 cloudpulse/common/__init__.py create mode 100644 cloudpulse/objects/__init__.py create mode 100644 cloudpulse/objects/base.py create mode 100644 cloudpulse/objects/cpulse.py create mode 100644 cloudpulse/objects/fields.py create mode 100644 cloudpulse/objects/utils.py diff --git a/cloudpulse/common/__init__.py b/cloudpulse/common/__init__.py new file mode 100644 index 0000000..87e7bbe --- /dev/null +++ b/cloudpulse/common/__init__.py @@ -0,0 +1,19 @@ +# 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 pbr.version +import threading + +__version__ = pbr.version.VersionInfo('cloudpulse').version_string() + +# Make a project global TLS trace storage repository +TLS = threading.local() diff --git a/cloudpulse/objects/__init__.py b/cloudpulse/objects/__init__.py new file mode 100644 index 0000000..78a0850 --- /dev/null +++ b/cloudpulse/objects/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +from cloudpulse.objects import cpulse + +Cpulse = cpulse.Cpulse + +__all__ = (Cpulse) diff --git a/cloudpulse/objects/base.py b/cloudpulse/objects/base.py new file mode 100644 index 0000000..e323672 --- /dev/null +++ b/cloudpulse/objects/base.py @@ -0,0 +1,108 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +"""Cloudpulse common internal object model""" + +from oslo_versionedobjects import base as ovoo_base +from oslo_versionedobjects import fields as ovoo_fields + +from cloudpulse.openstack.common import log as logging + +LOG = logging.getLogger('object') +remotable_classmethod = ovoo_base.remotable_classmethod +remotable = ovoo_base.remotable + + +class CloudpulseObjectRegistry(ovoo_base.VersionedObjectRegistry): + pass + + +class CloudpulseObject(ovoo_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 = 'cloudpulse_object' + OBJ_PROJECT_NAMESPACE = 'cloudpulse' + + def as_dict(self): + return dict((k, getattr(self, k)) + for k in self.fields + if hasattr(self, k)) + + +class CloudpulseObjectDictCompat(ovoo_base.VersionedObjectDictCompat): + pass + + +class CloudpulsePersistentObject(object): + """Mixin class for Persistent objects. + + This adds the fields that we use in common for all persistent objects. + + """ + fields = { + 'created_at': ovoo_fields.DateTimeField(nullable=True), + 'updated_at': ovoo_fields.DateTimeField(nullable=True), + } + + +class ObjectListBase(ovoo_base.ObjectListBase): + # TODO(xek): These are for transition to using the oslo base object + # and can be removed when we move to it. + fields = { + 'objects': list, + } + + def _attr_objects_to_primitive(self): + """Serialization of object list.""" + return [x.obj_to_primitive() for x in self.objects] + + def _attr_objects_from_primitive(self, value): + """Deserialization of object list.""" + objects = [] + for entity in value: + ctx = self._context + obj = CloudpulseObject.obj_from_primitive(entity, + context=ctx) + objects.append(obj) + return objects + + +class CloudpulseObjectSerializer(ovoo_base.VersionedObjectSerializer): + # Base class to use for object hydration + OBJ_BASE_CLASS = CloudpulseObject + + +def obj_to_primitive(obj): + """Recursively turn an object into a python primitive. + + An CloudpulseObject becomes a dict, and anything + that implements ObjectListBase becomes a list. + """ + if isinstance(obj, ObjectListBase): + return [obj_to_primitive(x) for x in obj] + elif isinstance(obj, CloudpulseObject): + result = {} + for key in obj.obj_fields: + if obj.obj_attr_is_set(key) or key in obj.obj_extra_fields: + result[key] = obj_to_primitive(getattr(obj, key)) + return result + else: + return obj diff --git a/cloudpulse/objects/cpulse.py b/cloudpulse/objects/cpulse.py new file mode 100644 index 0000000..278dbaf --- /dev/null +++ b/cloudpulse/objects/cpulse.py @@ -0,0 +1,205 @@ +# coding=utf-8 +# +# +# 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. + +from oslo_versionedobjects import fields + +from cloudpulse.common import exception +from cloudpulse.common import utils +from cloudpulse.db import api as dbapi +from cloudpulse.objects import base +from cloudpulse.openstack.common._i18n import _LI +from cloudpulse.openstack.common import log as logging +LOG = logging.getLogger(__name__) + + +class Status(object): + CREATE_IN_PROGRESS = 'CREATE_IN_PROGRESS' + CREATE_FAILED = 'CREATE_FAILED' + CREATED = 'CREATED' + UPDATE_IN_PROGRESS = 'UPDATE_IN_PROGRESS' + UPDATE_FAILED = 'UPDATE_FAILED' + UPDATED = 'UPDATED' + DELETE_IN_PROGRESS = 'DELETE_IN_PROGRESS' + DELETE_FAILED = 'DELETE_FAILED' + DELETED = 'DELETED' + + +@base.CloudpulseObjectRegistry.register +class Cpulse(base.CloudpulsePersistentObject, base.CloudpulseObject, + base.CloudpulseObjectDictCompat): + # Version 1.0: Initial version + VERSION = '1.0' + + dbapi = dbapi.get_instance() + + fields = { + 'id': fields.IntegerField(), + 'uuid': fields.UUIDField(nullable=True), + 'name': fields.StringField(nullable=True), + 'state': fields.StringField(nullable=True), + 'result': fields.StringField(nullable=True) + } + + @staticmethod + def _from_db_object(test, db): + """Converts a database entity to a formal object.""" + for field in test.fields: + test[field] = db[field] + + test.obj_reset_changes() + return test + + @staticmethod + def _from_db_object_list(db_objects, cls, ctx): + """Converts a list of db entities to a list of formal objects.""" + return [Cpulse._from_db_object(cls(ctx), obj) for obj in db_objects] + + @base.remotable_classmethod + def get(cls, context, test_id): + """Find a test based on its id or uuid and return a Cpulse object. + + :param test_id: the id *or* uuid of a test. + :returns: a :class:`Cpulse` object. + """ + if utils.is_int_like(test_id): + return cls.get_by_id(context, test_id) + elif utils.is_uuid_like(test_id): + return cls.get_by_uuid(context, test_id) + else: + raise exception.InvalidIdentity(identity=test_id) + + @base.remotable_classmethod + def get_by_id(cls, context, test_id): + """Find a test based on its integer id and return a Cpulse object. + + :param test_id: the id of a test. + :returns: a :class:`Cpulse` object. + """ + db = cls.dbapi.get_test_by_id(context, test_id) + test = Cpulse._from_db_object(cls(context), db) + return test + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + """Find a test based on uuid and return a :class:`Cpulse` object. + + :param uuid: the uuid of a test. + :param context: Security context + :returns: a :class:`Cpulse` object. + """ + db = cls.dbapi.get_test_by_uuid(context, uuid) + test = Cpulse._from_db_object(cls(context), db) + return test + + @base.remotable_classmethod + def get_by_name(cls, context, name): + """Find a test based on name and return a Cpulse object. + + :param name: the logical name of a test. + :param context: Security context + :returns: a :class:`Cpulse` object. + """ + db = cls.dbapi.get_test_by_name(context, name) + test = Cpulse._from_db_object(cls(context), db) + return test + + @base.remotable_classmethod + def list(cls, context, limit=None, marker=None, + sort_key=None, sort_dir=None): + """Return a list of Cpulse objects. + + :param context: Security context. + :param limit: maximum number of resources to return in a single result. + :param marker: pagination marker for large data sets. + :param sort_key: column to sort results by. + :param sort_dir: direction to sort. "asc" or "desc". + :returns: a list of :class:`Cpulse` object. + + """ + db = cls.dbapi.get_test_list(context, limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + return Cpulse._from_db_object_list(db, cls, context) + + @base.remotable + def create(self, context=None): + """Create a Cpulse record in the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Cpulse(context) + + """ + values = self.obj_get_changes() + LOG.info(_LI('Dumping CREATE test datastructure %s') % str(values)) + db = self.dbapi.create_test(values) + self._from_db_object(self, db) + + @base.remotable + def destroy(self, context=None): + """Delete the Cpulse from the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Cpulse(context) + """ + self.dbapi.destroy_test(self.uuid) + self.obj_reset_changes() + + @base.remotable + def save(self, context=None): + """Save updates to this Cpulse. + + Updates will be made column by column based on the result + of self.what_changed(). + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Cpulse(context) + """ + updates = self.obj_get_changes() + self.dbapi.update_test(self.uuid, updates) + + self.obj_reset_changes() + + @base.remotable + def refresh(self, context=None): + """Loads updates for this Cpulse. + + Loads a test with the same uuid from the database and + checks for updated attributes. Updates are applied from + the loaded test column by column, if there are any updates. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Cpulse(context) + """ + current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) + for field in self.fields: + if self.obj_attr_is_set(field) and self[field] != current[field]: + self[field] = current[field] diff --git a/cloudpulse/objects/fields.py b/cloudpulse/objects/fields.py new file mode 100644 index 0000000..189c13c --- /dev/null +++ b/cloudpulse/objects/fields.py @@ -0,0 +1,19 @@ +# Copyright 2015 Intel Corp. +# +# 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. + +from oslo_versionedobjects import fields + + +class ListOfDictsField(fields.AutoTypedField): + AUTO_TYPE = fields.List(fields.Dict(fields.FieldType())) diff --git a/cloudpulse/objects/utils.py b/cloudpulse/objects/utils.py new file mode 100644 index 0000000..71ad113 --- /dev/null +++ b/cloudpulse/objects/utils.py @@ -0,0 +1,134 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +"""Utility methods for objects""" + +import ast +import datetime + +import iso8601 +import netaddr +from oslo_utils import timeutils +import six + +from cloudpulse.openstack.common._i18n import _ + + +def datetime_or_none(dt): + """Validate a datetime or None value.""" + if dt is None: + return None + elif isinstance(dt, datetime.datetime): + if dt.utcoffset() is None: + # NOTE(danms): Legacy objects from sqlalchemy are stored in UTC, + # but are returned without a timezone attached. + # As a transitional aid, assume a tz-naive object is in UTC. + return dt.replace(tzinfo=iso8601.iso8601.Utc()) + else: + return dt + raise ValueError(_("A datetime.datetime is required here")) + + +def datetime_or_str_or_none(val): + if isinstance(val, six.string_types): + return timeutils.parse_isotime(val) + return datetime_or_none(val) + + +def int_or_none(val): + """Attempt to parse an integer value, or None.""" + if val is None: + return val + else: + return int(val) + + +def str_or_none(val): + """Attempt to stringify a value to unicode, or None.""" + if val is None: + return val + else: + return six.text_type(val) + + +def dict_or_none(val): + """Attempt to dictify a value, or None.""" + if val is None: + return {} + elif isinstance(val, six.string_types): + return dict(ast.literal_eval(val)) + else: + try: + return dict(val) + except ValueError: + return {} + + +def list_or_none(val): + """Attempt to listify a value, or None.""" + if val is None: + return [] + elif isinstance(val, six.string_types): + return list(ast.literal_eval(val)) + else: + try: + return list(val) + except ValueError: + return [] + + +def ip_or_none(version): + """Return a version-specific IP address validator.""" + def validator(val, version=version): + if val is None: + return val + else: + return netaddr.IPAddress(val, version=version) + return validator + + +def nested_object_or_none(objclass): + def validator(val, objclass=objclass): + if val is None or isinstance(val, objclass): + return val + raise ValueError(_("An object of class %s is required here") + % objclass) + return validator + + +def dt_serializer(name): + """Return a datetime serializer for a named attribute.""" + def serializer(self, name=name): + if getattr(self, name) is not None: + return timeutils.isotime(getattr(self, name)) + else: + return None + return serializer + + +def dt_deserializer(instance, val): + """A deserializer method for datetime attributes.""" + if val is None: + return None + else: + return timeutils.parse_isotime(val) + + +def obj_serializer(name): + def serializer(self, name=name): + if getattr(self, name) is not None: + return getattr(self, name).obj_to_primitive() + else: + return None + return serializer diff --git a/requirements.txt b/requirements.txt index f9b5f9a..aff346e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,11 @@ oslo.serialization>=1.4.0,<1.5.0 # Apache-2.0 oslo.utils>=1.4.0,<1.5.0 # Apache-2.0 oslo.versionedobjects>=0.1.1,<0.2.0 oslo.i18n>=1.5.0,<1.6.0 # Apache-2.0 +paramiko>=1.13.0 +pecan>=0.8.0 +python-keystoneclient>=1.1.0 six>=1.9.0 SQLAlchemy>=0.9.7,<=0.9.99 taskflow>=0.7.1,<0.8.0 +WSME>=0.6