Merge "Validate backup size during restore"
This commit is contained in:
commit
5f252e3bc5
@ -509,3 +509,9 @@ class TroveOperationAuthError(TroveError):
|
||||
class ClusterDatastoreNotSupported(TroveError):
|
||||
message = _("Clusters not supported for "
|
||||
"%(datastore)s-%(datastore_version)s.")
|
||||
|
||||
|
||||
class BackupTooLarge(TroveError):
|
||||
message = _("Backup is too large for given flavor or volume. "
|
||||
"Backup size: %(backup_size)s GBs. "
|
||||
"Available size: %(disk_size)s GBs.")
|
||||
|
@ -319,6 +319,7 @@ class Controller(object):
|
||||
],
|
||||
webob.exc.HTTPForbidden: [
|
||||
exception.ReplicaSourceDeleteForbidden,
|
||||
exception.BackupTooLarge,
|
||||
],
|
||||
webob.exc.HTTPBadRequest: [
|
||||
exception.InvalidModelError,
|
||||
|
@ -15,7 +15,6 @@
|
||||
# under the License.
|
||||
|
||||
"""Model classes that form the core of instances functionality."""
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from novaclient import exceptions as nova_exceptions
|
||||
@ -677,19 +676,27 @@ class Instance(BuiltInstance):
|
||||
if volume_support:
|
||||
validate_volume_size(volume_size)
|
||||
deltas['volumes'] = volume_size
|
||||
# Instance volume should have enough space for the backup
|
||||
# Backup, and volume sizes are in GBs
|
||||
target_size = volume_size
|
||||
else:
|
||||
target_size = flavor.disk # local_storage
|
||||
if volume_size is not None:
|
||||
raise exception.VolumeNotSupported()
|
||||
ephemeral_support = datastore_cfg.device_path
|
||||
if ephemeral_support:
|
||||
if datastore_cfg.device_path:
|
||||
if flavor.ephemeral == 0:
|
||||
raise exception.LocalStorageNotSpecified(flavor=flavor_id)
|
||||
target_size = flavor.ephemeral # ephemeral_Storage
|
||||
|
||||
if backup_id is not None:
|
||||
backup_info = Backup.get_by_id(context, backup_id)
|
||||
if backup_info.is_running:
|
||||
raise exception.BackupNotCompleteError(backup_id=backup_id)
|
||||
|
||||
if backup_info.size > target_size:
|
||||
raise exception.BackupTooLarge(
|
||||
backup_size=backup_info.size, disk_size=target_size)
|
||||
|
||||
if not backup_info.check_swift_object_exist(
|
||||
context,
|
||||
verify_checksum=CONF.verify_swift_checksum_on_restore):
|
||||
|
@ -19,6 +19,7 @@ from proboscis.asserts import fail
|
||||
from proboscis import test
|
||||
from proboscis import SkipTest
|
||||
from proboscis.decorators import time_out
|
||||
from trove.common import cfg
|
||||
from trove.common.utils import poll_until
|
||||
from trove.common.utils import generate_uuid
|
||||
from trove.common import exception
|
||||
@ -136,9 +137,63 @@ class AfterBackupCreation(object):
|
||||
assert_unprocessable(instance_info.dbaas.backups.delete, backup.id)
|
||||
|
||||
|
||||
class BackupRestoreMixin():
|
||||
|
||||
def verify_backup(self, backup_id):
|
||||
def result_is_active():
|
||||
backup = instance_info.dbaas.backups.get(backup_id)
|
||||
if backup.status == "COMPLETED":
|
||||
return True
|
||||
else:
|
||||
assert_not_equal("FAILED", backup.status)
|
||||
return False
|
||||
|
||||
poll_until(result_is_active)
|
||||
|
||||
def instance_is_totally_gone(self, instance_id):
|
||||
|
||||
def instance_is_gone():
|
||||
try:
|
||||
instance_info.dbaas.instances.get(
|
||||
instance_id)
|
||||
return False
|
||||
except exceptions.NotFound:
|
||||
return True
|
||||
|
||||
poll_until(
|
||||
instance_is_gone, time_out=TIMEOUT_INSTANCE_DELETE)
|
||||
|
||||
def backup_is_totally_gone(self, backup_id):
|
||||
def backup_is_gone():
|
||||
try:
|
||||
instance_info.dbaas.backups.get(backup_id)
|
||||
return False
|
||||
except exceptions.NotFound:
|
||||
return True
|
||||
|
||||
poll_until(backup_is_gone, time_out=TIMEOUT_BACKUP_DELETE)
|
||||
|
||||
def verify_instance_is_active(self, instance_id):
|
||||
# This version just checks the REST API status.
|
||||
def result_is_active():
|
||||
instance = instance_info.dbaas.instances.get(instance_id)
|
||||
if instance.status == "ACTIVE":
|
||||
return True
|
||||
else:
|
||||
# If its not ACTIVE, anything but BUILD must be
|
||||
# an error.
|
||||
assert_equal("BUILD", instance.status)
|
||||
if instance_info.volume is not None:
|
||||
assert_equal(instance.volume.get('used', None), None)
|
||||
return False
|
||||
|
||||
poll_until(result_is_active, sleep_time=5,
|
||||
time_out=TIMEOUT_INSTANCE_CREATE)
|
||||
|
||||
|
||||
@test(runs_after=[AfterBackupCreation],
|
||||
groups=[GROUP, tests.INSTANCES])
|
||||
class WaitForBackupCreateToFinish(object):
|
||||
class WaitForBackupCreateToFinish(BackupRestoreMixin):
|
||||
"""
|
||||
Wait until the backup create is finished.
|
||||
"""
|
||||
@ -147,15 +202,7 @@ class WaitForBackupCreateToFinish(object):
|
||||
@time_out(TIMEOUT_BACKUP_CREATE)
|
||||
def test_backup_created(self):
|
||||
# This version just checks the REST API status.
|
||||
def result_is_active():
|
||||
backup = instance_info.dbaas.backups.get(backup_info.id)
|
||||
if backup.status == "COMPLETED":
|
||||
return True
|
||||
else:
|
||||
assert_not_equal("FAILED", backup.status)
|
||||
return False
|
||||
|
||||
poll_until(result_is_active)
|
||||
self.verify_backup(backup_info.id)
|
||||
|
||||
|
||||
@test(depends_on=[WaitForBackupCreateToFinish],
|
||||
@ -176,7 +223,7 @@ class ListBackups(object):
|
||||
|
||||
@test
|
||||
def test_backup_list_filter_datastore(self):
|
||||
"""test list backups and filter by datastore."""
|
||||
"""Test list backups and filter by datastore."""
|
||||
result = instance_info.dbaas.backups.list(
|
||||
datastore=instance_info.dbaas_datastore)
|
||||
assert_equal(backup_count_prior_to_create + 1, len(result))
|
||||
@ -189,7 +236,7 @@ class ListBackups(object):
|
||||
|
||||
@test
|
||||
def test_backup_list_filter_different_datastore(self):
|
||||
"""test list backups and filter by datastore."""
|
||||
"""Test list backups and filter by datastore."""
|
||||
result = instance_info.dbaas.backups.list(
|
||||
datastore='Test_Datastore_1')
|
||||
# There should not be any backups for this datastore
|
||||
@ -197,7 +244,7 @@ class ListBackups(object):
|
||||
|
||||
@test
|
||||
def test_backup_list_filter_datastore_not_found(self):
|
||||
"""test list backups and filter by datastore."""
|
||||
"""Test list backups and filter by datastore."""
|
||||
assert_raises(exceptions.BadRequest, instance_info.dbaas.backups.list,
|
||||
datastore='NOT_FOUND')
|
||||
|
||||
@ -248,7 +295,7 @@ class ListBackups(object):
|
||||
@test(runs_after=[ListBackups],
|
||||
depends_on=[WaitForBackupCreateToFinish],
|
||||
groups=[GROUP, tests.INSTANCES])
|
||||
class IncrementalBackups(object):
|
||||
class IncrementalBackups(BackupRestoreMixin):
|
||||
|
||||
@test
|
||||
def test_create_db(self):
|
||||
@ -270,15 +317,7 @@ class IncrementalBackups(object):
|
||||
assert_equal(202, instance_info.dbaas.last_http_code)
|
||||
|
||||
# Wait for the backup to finish
|
||||
def result_is_active():
|
||||
backup = instance_info.dbaas.backups.get(incremental_info.id)
|
||||
if backup.status == "COMPLETED":
|
||||
return True
|
||||
else:
|
||||
assert_not_equal("FAILED", backup.status)
|
||||
return False
|
||||
|
||||
poll_until(result_is_active, time_out=TIMEOUT_BACKUP_CREATE)
|
||||
self.verify_backup(incremental_info.id)
|
||||
assert_equal(backup_info.id, incremental_info.parent_id)
|
||||
|
||||
|
||||
@ -457,3 +496,73 @@ class DeleteBackups(object):
|
||||
raise SkipTest("Incremental Backup not created")
|
||||
assert_raises(exceptions.NotFound, instance_info.dbaas.backups.get,
|
||||
incremental_info.id)
|
||||
|
||||
|
||||
@test(depends_on=[WaitForGuestInstallationToFinish],
|
||||
runs_after=[DeleteBackups])
|
||||
class FakeTestHugeBackupOnSmallInstance(BackupRestoreMixin):
|
||||
|
||||
report = CONFIG.get_report()
|
||||
|
||||
def tweak_fake_guest(self, size):
|
||||
from trove.tests.fakes import guestagent
|
||||
guestagent.BACKUP_SIZE = size
|
||||
|
||||
@test
|
||||
def test_load_mysql_with_data(self):
|
||||
if not CONFIG.fake_mode:
|
||||
raise SkipTest("Must run in fake mode.")
|
||||
self.tweak_fake_guest(1.9)
|
||||
|
||||
@test(depends_on=[test_load_mysql_with_data])
|
||||
def test_create_huge_backup(self):
|
||||
if not CONFIG.fake_mode:
|
||||
raise SkipTest("Must run in fake mode.")
|
||||
self.new_backup = instance_info.dbaas.backups.create(
|
||||
BACKUP_NAME,
|
||||
instance_info.id,
|
||||
BACKUP_DESC)
|
||||
|
||||
assert_equal(202, instance_info.dbaas.last_http_code)
|
||||
|
||||
@test(depends_on=[test_create_huge_backup])
|
||||
def test_verify_huge_backup_completed(self):
|
||||
if not CONFIG.fake_mode:
|
||||
raise SkipTest("Must run in fake mode.")
|
||||
self.verify_backup(self.new_backup.id)
|
||||
|
||||
@test(depends_on=[test_verify_huge_backup_completed])
|
||||
def test_try_to_restore_on_small_instance_with_volume(self):
|
||||
if not CONFIG.fake_mode:
|
||||
raise SkipTest("Must run in fake mode.")
|
||||
assert_raises(exceptions.Forbidden,
|
||||
instance_info.dbaas.instances.create,
|
||||
instance_info.name + "_restore",
|
||||
instance_info.dbaas_flavor_href,
|
||||
{'size': 1},
|
||||
datastore=instance_info.dbaas_datastore,
|
||||
datastore_version=(instance_info.
|
||||
dbaas_datastore_version),
|
||||
restorePoint={"backupRef": self.new_backup.id})
|
||||
assert_equal(403, instance_info.dbaas.last_http_code)
|
||||
|
||||
@test(depends_on=[test_verify_huge_backup_completed])
|
||||
def test_try_to_restore_on_small_instance_with_flavor_only(self):
|
||||
if not CONFIG.fake_mode:
|
||||
raise SkipTest("Must run in fake mode.")
|
||||
self.orig_conf_value = cfg.CONF.get(
|
||||
instance_info.dbaas_datastore).volume_support
|
||||
cfg.CONF.get(instance_info.dbaas_datastore).volume_support = False
|
||||
|
||||
assert_raises(exceptions.Forbidden,
|
||||
instance_info.dbaas.instances.create,
|
||||
instance_info.name + "_restore", 11,
|
||||
datastore=instance_info.dbaas_datastore,
|
||||
datastore_version=(instance_info.
|
||||
dbaas_datastore_version),
|
||||
restorePoint={"backupRef": self.new_backup.id})
|
||||
|
||||
assert_equal(403, instance_info.dbaas.last_http_code)
|
||||
cfg.CONF.get(
|
||||
instance_info.dbaas_datastore
|
||||
).volume_support = self.orig_conf_value
|
||||
|
@ -24,6 +24,7 @@ from trove.tests.util import unquote_user_host
|
||||
|
||||
DB = {}
|
||||
LOG = logging.getLogger(__name__)
|
||||
BACKUP_SIZE = 0.14
|
||||
|
||||
|
||||
class FakeGuest(object):
|
||||
@ -306,6 +307,7 @@ class FakeGuest(object):
|
||||
backup.state = BackupState.COMPLETED
|
||||
backup.location = 'http://localhost/path/to/backup'
|
||||
backup.checksum = 'fake-md5-sum'
|
||||
backup.size = BACKUP_SIZE
|
||||
backup.save()
|
||||
eventlet.spawn_after(1.0, finish_create_backup)
|
||||
|
||||
|
@ -11,17 +11,26 @@
|
||||
# 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 uuid
|
||||
from mock import Mock
|
||||
from testtools import TestCase
|
||||
from trove.common import cfg
|
||||
from trove.common import exception
|
||||
from trove.common.instance import ServiceStatuses
|
||||
from trove.backup import models as backup_models
|
||||
from trove.datastore import models as datastore_models
|
||||
from trove.instance.models import filter_ips
|
||||
from trove.instance.models import InstanceServiceStatus
|
||||
from trove.instance.models import DBInstance
|
||||
from trove.instance.models import Instance
|
||||
from trove.instance.models import SimpleInstance
|
||||
from trove.instance import models
|
||||
from trove.instance.tasks import InstanceTasks
|
||||
from trove.taskmanager import api as task_api
|
||||
|
||||
|
||||
from trove.tests.fakes import nova
|
||||
from trove.tests.unittests.util import util
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
@ -89,3 +98,112 @@ class SimpleInstanceTest(TestCase):
|
||||
self.assertTrue('10.123.123.123' in ip)
|
||||
self.assertTrue('123.123.123.123' in ip)
|
||||
self.assertTrue('15.123.123.123' in ip)
|
||||
|
||||
|
||||
class CreateInstanceTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
util.init_db()
|
||||
self.context = Mock()
|
||||
self.name = "name"
|
||||
self.flavor_id = 5
|
||||
self.image_id = "UUID"
|
||||
self.databases = []
|
||||
self.users = []
|
||||
self.datastore = datastore_models.DBDatastore.create(
|
||||
id=str(uuid.uuid4()),
|
||||
name='mysql',
|
||||
)
|
||||
self.datastore_version = (
|
||||
datastore_models.DBDatastoreVersion.create(
|
||||
id=str(uuid.uuid4()),
|
||||
datastore_id=self.datastore.id,
|
||||
name="5.5",
|
||||
manager="mysql",
|
||||
image_id="image_id",
|
||||
packages="",
|
||||
active=True))
|
||||
self.volume_size = 1
|
||||
self.az = "az"
|
||||
self.nics = None
|
||||
self.configuration = None
|
||||
self.tenant_id = "UUID"
|
||||
self.datastore_version_id = str(uuid.uuid4())
|
||||
|
||||
self.db_info = DBInstance.create(
|
||||
name=self.name, flavor_id=self.flavor_id,
|
||||
tenant_id=self.tenant_id,
|
||||
volume_size=self.volume_size,
|
||||
datastore_version_id=
|
||||
self.datastore_version.id,
|
||||
task_status=InstanceTasks.BUILDING,
|
||||
configuration_id=self.configuration
|
||||
)
|
||||
|
||||
self.backup_name = "name"
|
||||
self.descr = None
|
||||
self.backup_state = backup_models.BackupState.COMPLETED
|
||||
self.instance_id = self.db_info.id
|
||||
self.parent_id = None
|
||||
self.deleted = False
|
||||
|
||||
self.backup = backup_models.DBBackup.create(
|
||||
name=self.backup_name,
|
||||
description=self.descr,
|
||||
tenant_id=self.tenant_id,
|
||||
state=self.backup_state,
|
||||
instance_id=self.instance_id,
|
||||
parent_id=self.parent_id,
|
||||
datastore_version_id=self.datastore_version.id,
|
||||
deleted=False
|
||||
)
|
||||
self.backup.size = 1.1
|
||||
self.backup.save()
|
||||
self.backup_id = self.backup.id
|
||||
self.orig_client = models.create_nova_client
|
||||
models.create_nova_client = nova.fake_create_nova_client
|
||||
self.orig_api = task_api.API(self.context).create_instance
|
||||
task_api.API(self.context).create_instance = Mock()
|
||||
self.run_with_quotas = models.run_with_quotas
|
||||
models.run_with_quotas = Mock()
|
||||
self.check = backup_models.DBBackup.check_swift_object_exist
|
||||
backup_models.DBBackup.check_swift_object_exist = Mock(
|
||||
return_value=True)
|
||||
super(CreateInstanceTest, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
self.db_info.delete()
|
||||
self.backup.delete()
|
||||
self.datastore.delete()
|
||||
self.datastore_version.delete()
|
||||
models.create_nova_client = self.orig_client
|
||||
task_api.API(self.context).create_instance = self.orig_api
|
||||
models.run_with_quotas = self.run_with_quotas
|
||||
backup_models.DBBackup.check_swift_object_exist = self.context
|
||||
self.backup.delete()
|
||||
self.db_info.delete()
|
||||
super(CreateInstanceTest, self).tearDown()
|
||||
|
||||
def test_exception_on_invalid_backup_size(self):
|
||||
exc = self.assertRaises(
|
||||
exception.BackupTooLarge, models.Instance.create,
|
||||
self.context, self.name, self.flavor_id,
|
||||
self.image_id, self.databases, self.users,
|
||||
self.datastore, self.datastore_version,
|
||||
self.volume_size, self.backup_id,
|
||||
self.az, self.nics, self.configuration
|
||||
)
|
||||
self.assertIn("Backup is too large for "
|
||||
"given flavor or volume.", str(exc))
|
||||
|
||||
def test_can_restore_from_backup_with_almost_equal_size(self):
|
||||
#target size equals to "1Gb"
|
||||
self.backup.size = 0.99
|
||||
self.backup.save()
|
||||
instance = models.Instance.create(
|
||||
self.context, self.name, self.flavor_id,
|
||||
self.image_id, self.databases, self.users,
|
||||
self.datastore, self.datastore_version,
|
||||
self.volume_size, self.backup_id,
|
||||
self.az, self.nics, self.configuration)
|
||||
self.assertIsNotNone(instance)
|
||||
|
Loading…
x
Reference in New Issue
Block a user