The versioned object and db operation of Restores
Change-Id: I5705aea6a8fccdfa30ea3a4f9fe6f4730cc1cd58 Closes-Bug: #1545624
This commit is contained in:
parent
78244d1e0a
commit
800ee3a744
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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
107
smaug/objects/restore.py
Normal 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)
|
@ -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, {})
|
||||
|
41
smaug/tests/unit/fake_restore.py
Normal file
41
smaug/tests/unit/fake_restore.py
Normal 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
|
69
smaug/tests/unit/objects/test_restore.py
Normal file
69
smaug/tests/unit/objects/test_restore.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user