diff --git a/trove/common/exception.py b/trove/common/exception.py index a6750768bf..4b123773df 100644 --- a/trove/common/exception.py +++ b/trove/common/exception.py @@ -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.") diff --git a/trove/common/wsgi.py b/trove/common/wsgi.py index 766d59607e..febdc49747 100644 --- a/trove/common/wsgi.py +++ b/trove/common/wsgi.py @@ -319,6 +319,7 @@ class Controller(object): ], webob.exc.HTTPForbidden: [ exception.ReplicaSourceDeleteForbidden, + exception.BackupTooLarge, ], webob.exc.HTTPBadRequest: [ exception.InvalidModelError, diff --git a/trove/instance/models.py b/trove/instance/models.py index c44d9bf4a7..c9a126c354 100644 --- a/trove/instance/models.py +++ b/trove/instance/models.py @@ -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): diff --git a/trove/tests/api/backups.py b/trove/tests/api/backups.py index 2ebd7f705b..4099e99cee 100644 --- a/trove/tests/api/backups.py +++ b/trove/tests/api/backups.py @@ -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 diff --git a/trove/tests/fakes/guestagent.py b/trove/tests/fakes/guestagent.py index e776e59efe..473d352673 100644 --- a/trove/tests/fakes/guestagent.py +++ b/trove/tests/fakes/guestagent.py @@ -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) diff --git a/trove/tests/unittests/instance/test_instance_models.py b/trove/tests/unittests/instance/test_instance_models.py index 1160bc5cd8..d67db5f745 100644 --- a/trove/tests/unittests/instance/test_instance_models.py +++ b/trove/tests/unittests/instance/test_instance_models.py @@ -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)