The versioned object and db operation of Restores

Change-Id: I5705aea6a8fccdfa30ea3a4f9fe6f4730cc1cd58
Closes-Bug: #1545624
This commit is contained in:
chenying 2016-02-18 17:10:38 +08:00
parent 78244d1e0a
commit 800ee3a744
9 changed files with 513 additions and 3 deletions

View File

@ -402,3 +402,46 @@ def plan_get_all_by_project(context, project_id, marker, limit,
sort_dirs=sort_dirs,
filters=filters,
offset=offset)
def restore_get(context, restore_id):
"""Get a restore or raise if it does not exist."""
return IMPL.restore_get(context, restore_id)
def restore_create(context, values):
"""Create a restore from the values dictionary."""
return IMPL.restore_create(context, values)
def restore_update(context, restore_id, values):
"""Set the given properties on a restore and update it.
Raises NotFound if plan does not exist.
"""
return IMPL.restore_update(context, restore_id, values)
def restore_destroy(context, restore_id):
"""Destroy the restore or raise if it does not exist."""
return IMPL.restore_destroy(context, restore_id)
def restore_get_all(context, marker, limit, sort_keys=None, sort_dirs=None,
filters=None, offset=None):
"""Get all restores."""
return IMPL.restore_get_all(context, marker, limit, sort_keys=sort_keys,
sort_dirs=sort_dirs, filters=filters,
offset=offset)
def restore_get_all_by_project(context, project_id, marker, limit,
sort_keys=None, sort_dirs=None, filters=None,
offset=None):
"""Get all restores belonging to a project."""
return IMPL.restore_get_all_by_project(context, project_id, marker, limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
filters=filters,
offset=offset)

View File

@ -825,8 +825,172 @@ def _process_plan_filters(query, filters):
###############################
@require_context
def restore_create(context, values):
restore_ref = models.Restore()
if not values.get('id'):
values['id'] = str(uuid.uuid4())
restore_ref.update(values)
session = get_session()
with session.begin():
restore_ref.save(session)
return restore_ref
@require_context
def restore_get(context, restore_id):
return _restore_get(context, restore_id)
@require_context
def _restore_get(context, restore_id, session=None):
result = model_query(
context,
models.Restore,
session=session).\
filter_by(id=restore_id).\
first()
if not result:
raise exception.RestoreNotFound(restore_id=restore_id)
return result
@require_context
def restore_update(context, restore_id, values):
session = get_session()
with session.begin():
restore_ref = _restore_get(context, restore_id, session=session)
restore_ref.update(values)
return restore_ref
@require_context
@_retry_on_deadlock
def restore_destroy(context, restore_id):
session = get_session()
with session.begin():
restore_ref = _restore_get(context, restore_id, session=session)
restore_ref.delete(session=session)
def is_valid_model_filters(model, filters):
"""Return True if filter values exist on the model
:param model: a smaug model
:param filters: dictionary of filters
"""
for key in filters.keys():
try:
getattr(model, key)
except AttributeError:
LOG.debug("'%s' filter key is not valid.", key)
return False
return True
def _restore_get_query(context, session=None, project_only=False):
return model_query(context, models.Restore, session=session,
project_only=project_only)
@require_admin_context
def restore_get_all(context, marker, limit, sort_keys=None, sort_dirs=None,
filters=None, offset=None):
"""Retrieves all restores.
If no sort parameters are specified then the returned plans are sorted
first by the 'created_at' key and then by the 'id' key in descending
order.
:param context: context to query under
:param marker: the last item of the previous page, used to determine the
next page of results to return
:param limit: maximum number of items to return
:param sort_keys: list of attributes by which results should be sorted,
paired with corresponding item in sort_dirs
:param sort_dirs: list of directions in which results should be sorted,
paired with corresponding item in sort_keys
:param filters: dictionary of filters; values that are in lists, tuples,
or sets cause an 'IN' operation, while exact matching
is used for other values, see _process_plan_filters
function for more information
:returns: list of matching restores
"""
if filters and not is_valid_model_filters(models.Restore, filters):
return []
session = get_session()
with session.begin():
# Generate the query
query = _generate_paginate_query(context, session, marker, limit,
sort_keys, sort_dirs, filters,
offset, models.Restore)
# No restores would match, return empty list
if query is None:
return []
return query.all()
@require_context
def restore_get_all_by_project(context, project_id, marker, limit,
sort_keys=None, sort_dirs=None, filters=None,
offset=None):
"""Retrieves all restores in a project.
If no sort parameters are specified then the returned plans are sorted
first by the 'created_at' key and then by the 'id' key in descending
order.
:param context: context to query under
:param project_id: project for all plans being retrieved
:param marker: the last item of the previous page, used to determine the
next page of results to return
:param limit: maximum number of items to return
:param sort_keys: list of attributes by which results should be sorted,
paired with corresponding item in sort_dirs
:param sort_dirs: list of directions in which results should be sorted,
paired with corresponding item in sort_keys
:param filters: dictionary of filters; values that are in lists, tuples,
or sets cause an 'IN' operation, while exact matching
is used for other values, see _process_plan_filters
function for more information
:returns: list of matching restores
"""
if filters and not is_valid_model_filters(models.Restore, filters):
return []
session = get_session()
with session.begin():
authorize_project_context(context, project_id)
# Add in the project filter without modifying the given filters
filters = filters.copy() if filters else {}
filters['project_id'] = project_id
# Generate the query
query = _generate_paginate_query(context, session, marker, limit,
sort_keys, sort_dirs, filters,
offset, models.Restore)
# No plans would match, return empty list
if query is None:
return []
return query.all()
def _process_restore_filters(query, filters):
if filters:
# Ensure that filters' keys exist on the model
if not is_valid_model_filters(models.Restore, filters):
return None
query = query.filter_by(**filters)
return query
###############################
PAGINATION_HELPERS = {
models.Plan: (_plan_get_query, _process_plan_filters, _plan_get)
models.Plan: (_plan_get_query, _process_plan_filters, _plan_get),
models.Restore: (_restore_get_query, _process_restore_filters,
_restore_get)
}

View File

@ -155,6 +155,19 @@ class Resource(BASE, SmaugBase):
'Resource.deleted == False)')
class Restore(BASE, SmaugBase):
"""Represents a Restore."""
__tablename__ = 'restores'
id = Column(String(36), primary_key=True)
project_id = Column(String(255))
provider_id = Column(String(36))
checkpoint_id = Column(String(36))
restore_target = Column(String(255))
parameters = Column(String(255))
status = Column(String(64))
def register_models():
"""Register Models and create metadata.
@ -169,8 +182,8 @@ def register_models():
Trigger,
ScheduledOperation,
ScheduledOperationState,
ScheduledOperationLog)
ScheduledOperationLog,
Restore)
engine = create_engine(CONF.database.connection, echo=False)
for model in models:
model.metadata.create_all(engine)

View File

@ -208,5 +208,9 @@ class PlanNotFound(NotFound):
message = _("Plan %(plan_id)s could not be found.")
class RestoreNotFound(NotFound):
message = _("Restore %(restore_id)s could not be found.")
class InvalidPlan(Invalid):
message = _("Invalid plan: %(reason)s")

View File

@ -21,3 +21,4 @@ def register_all():
__import__('smaug.objects.trigger')
__import__('smaug.objects.scheduled_operation_log')
__import__('smaug.objects.scheduled_operation_state')
__import__('smaug.objects.restore')

107
smaug/objects/restore.py Normal file
View File

@ -0,0 +1,107 @@
# 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_config import cfg
from oslo_log import log as logging
from oslo_versionedobjects import fields
from smaug import db
from smaug import exception
from smaug.i18n import _
from smaug import objects
from smaug.objects import base
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
@base.SmaugObjectRegistry.register
class Restore(base.SmaugPersistentObject, base.SmaugObject,
base.SmaugObjectDictCompat,
base.SmaugComparableObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'id': fields.UUIDField(),
'project_id': fields.UUIDField(),
'provider_id': fields.UUIDField(),
'checkpoint_id': fields.UUIDField(),
'restore_target': fields.StringField(nullable=True),
'parameters': fields.StringField(nullable=True),
'status': fields.StringField(nullable=True),
}
@staticmethod
def _from_db_object(context, restore, db_restore):
for name, field in restore.fields.items():
value = db_restore.get(name)
if isinstance(field, fields.IntegerField):
value = value or 0
elif isinstance(field, fields.DateTimeField):
value = value or None
restore[name] = value
restore._context = context
restore.obj_reset_changes()
return restore
@base.remotable
def create(self):
if self.obj_attr_is_set('id'):
raise exception.ObjectActionError(action='create',
reason=_('already created'))
updates = self.smaug_obj_get_changes()
db_restore = db.restore_create(self._context, updates)
self._from_db_object(self._context, self, db_restore)
@base.remotable
def save(self):
updates = self.smaug_obj_get_changes()
if updates:
db.restore_update(self._context, self.id, updates)
self.obj_reset_changes()
@base.remotable
def destroy(self):
with self.obj_as_admin():
db.restore_destroy(self._context, self.id)
@base.SmaugObjectRegistry.register
class RestoreList(base.ObjectListBase, base.SmaugObject):
VERSION = '1.0'
fields = {
'objects': fields.ListOfObjectsField('Restore'),
}
@base.remotable_classmethod
def get_all(cls, context, marker, limit, sort_keys=None, sort_dirs=None,
filters=None, offset=None):
restores = db.restore_get_all(context, marker, limit,
sort_keys=sort_keys, sort_dirs=sort_dirs,
filters=filters, offset=offset)
return base.obj_make_list(context, cls(context), objects.Restore,
restores)
@base.remotable_classmethod
def get_all_by_project(cls, context, project_id, marker, limit,
sort_keys=None, sort_dirs=None, filters=None,
offset=None):
restores = db.restore_get_all_by_project(context, project_id, marker,
limit, sort_keys=sort_keys,
sort_dirs=sort_dirs,
filters=filters,
offset=offset)
return base.obj_make_list(context, cls(context), objects.Restore,
restores)

View File

@ -426,3 +426,71 @@ class PlanDbTestCase(base.TestCase):
db_meta = db.plan_resources_update(self.ctxt, plan["id"], resources2)
self.assertEqual("OS::Cinder::Volume", db_meta[0]["resource_type"])
class RestoreDbTestCase(base.TestCase):
"""Unit tests for smaug.db.api.restore_*."""
fake_restore = {
"id": "36ea41b2-c358-48a7-9117-70cb7617410a",
"project_id": "586cc6ce-e286-40bd-b2b5-dd32694d9944",
"provider_id": "2220f8b1-975d-4621-a872-fa9afb43cb6c",
"checkpoint_id": "09edcbdc-d1c2-49c1-a212-122627b20968",
"restore_target": "192.168.1.2:35357/v2.0",
"parameters": "{'username': 'admin'}",
"status": "SUCCESS"
}
def _dict_from_object(self, obj, ignored_keys):
if ignored_keys is None:
ignored_keys = []
if isinstance(obj, dict):
items = obj.items()
else:
items = obj.iteritems()
return {k: v for k, v in items
if k not in ignored_keys}
def _assertEqualObjects(self, obj1, obj2, ignored_keys=None):
obj1 = self._dict_from_object(obj1, ignored_keys)
obj2 = self._dict_from_object(obj2, ignored_keys)
self.assertEqual(
len(obj1), len(obj2),
"Keys mismatch: %s" % six.text_type(
set(obj1.keys()) ^ set(obj2.keys())))
for key, value in obj1.items():
self.assertEqual(value, obj2[key])
def setUp(self):
super(RestoreDbTestCase, self).setUp()
self.ctxt = context.get_admin_context()
def test_restore_create(self):
restore = db.restore_create(self.ctxt, self.fake_restore)
self.assertTrue(uuidutils.is_uuid_like(restore['id']))
self.assertEqual('SUCCESS', restore.status)
def test_restore_get(self):
restore = db.restore_create(self.ctxt,
self.fake_restore)
self._assertEqualObjects(restore, db.restore_get(self.ctxt,
restore['id']))
def test_restore_destroy(self):
restore = db.restore_create(self.ctxt, self.fake_restore)
db.restore_destroy(self.ctxt, restore['id'])
self.assertRaises(exception.RestoreNotFound, db.restore_get,
self.ctxt, restore['id'])
def test_restore_update(self):
restore = db.restore_create(self.ctxt, self.fake_restore)
db.restore_update(self.ctxt, restore['id'],
{'status': 'INIT'})
restore = db.restore_get(self.ctxt, restore['id'])
self.assertEqual('INIT', restore['status'])
def test_restore_update_nonexistent(self):
self.assertRaises(exception.RestoreNotFound, db.restore_update,
self.ctxt, 42, {})

View File

@ -0,0 +1,41 @@
# 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 smaug import objects
def fake_db_restore(**updates):
db_restore = {
"id": "36ea41b2-c358-48a7-9117-70cb7617410a",
"project_id": "586cc6ce-e286-40bd-b2b5-dd32694d9944",
"provider_id": "2220f8b1-975d-4621-a872-fa9afb43cb6c",
"checkpoint_id": "09edcbdc-d1c2-49c1-a212-122627b20968",
"restore_target": "192.168.1.2:35357/v2.0",
"parameters": "{'username': 'admin'}",
"status": "SUCCESS"
}
for name, field in objects.Restore.fields.items():
if name in db_restore:
continue
if field.nullable:
db_restore[name] = None
elif field.default != fields.UnspecifiedDefault:
db_restore[name] = field.default
else:
raise Exception('fake_db_restore needs help with %s.' % name)
if updates:
db_restore.update(updates)
return db_restore

View File

@ -0,0 +1,69 @@
# Copyright 2015 SimpliVity 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.
import mock
from smaug import objects
from smaug.tests.unit import fake_restore
from smaug.tests.unit import objects as test_objects
class TestRestore(test_objects.BaseObjectsTestCase):
@staticmethod
def _compare(test, db, obj):
db = {k: v for k, v in db.items()}
test_objects.BaseObjectsTestCase._compare(test, db, obj)
@mock.patch('smaug.objects.Restore.get_by_id')
def test_get_by_id(self, restore_get):
db_restore = fake_restore.fake_db_restore()
restore_get.return_value = db_restore
restore = objects.Restore.get_by_id(self.context, "1")
restore_get.assert_called_once_with(self.context, "1")
self._compare(self, db_restore, restore)
@mock.patch('smaug.db.sqlalchemy.api.restore_create')
def test_create(self, restore_create):
db_restore = fake_restore.fake_db_restore()
restore_create.return_value = db_restore
restore = objects.Restore(context=self.context)
restore.create()
self.assertEqual(db_restore['id'], restore.id)
@mock.patch('smaug.db.sqlalchemy.api.restore_update')
def test_save(self, restore_update):
db_restore = fake_restore.fake_db_restore()
restore = objects.Restore.\
_from_db_object(self.context,
objects.Restore(), db_restore)
restore.status = 'FAILED'
restore.save()
restore_update.assert_called_once_with(self.context, restore.id,
{'status': 'FAILED'})
@mock.patch('smaug.db.sqlalchemy.api.restore_destroy')
def test_destroy(self, restore_destroy):
db_restore = fake_restore.fake_db_restore()
restore = objects.Restore.\
_from_db_object(self.context,
objects.Restore(), db_restore)
restore.destroy()
self.assertTrue(restore_destroy.called)
admin_context = restore_destroy.call_args[0][0]
self.assertTrue(admin_context.is_admin)
def test_obj_field_status(self):
restore = objects.Restore(context=self.context,
status='FAILED')
self.assertEqual('FAILED', restore.status)