diff --git a/releasenotes/notes/restrict-launch-fields-on-restore-from-backup-8a32174e27f185dc.yaml b/releasenotes/notes/restrict-launch-fields-on-restore-from-backup-8a32174e27f185dc.yaml new file mode 100644 index 0000000..6ee86a9 --- /dev/null +++ b/releasenotes/notes/restrict-launch-fields-on-restore-from-backup-8a32174e27f185dc.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - Adds support for restricting the launch instance datastore field + to the datastore and datastore version that the backup is + relevant to. It also populates the restored backup as the only + option in the advanced step backup field. diff --git a/trove_dashboard/content/database_backups/tests.py b/trove_dashboard/content/database_backups/tests.py index 2db9a04..4b5a6b1 100644 --- a/trove_dashboard/content/database_backups/tests.py +++ b/trove_dashboard/content/database_backups/tests.py @@ -12,19 +12,27 @@ # License for the specific language governing permissions and limitations # under the License. +import binascii + from django.core.urlresolvers import reverse from django import http +from django.utils.translation import ugettext_lazy as _ from mox3.mox import IsA # noqa import six from openstack_auth import policy +from openstack_dashboard import api as dash_api + +from troveclient import common from trove_dashboard import api +from trove_dashboard.content.databases.workflows import create_instance from trove_dashboard.test import helpers as test INDEX_URL = reverse('horizon:project:database_backups:index') BACKUP_URL = reverse('horizon:project:database_backups:create') DETAILS_URL = reverse('horizon:project:database_backups:detail', args=['id']) +RESTORE_URL = reverse('horizon:project:databases:launch') class DatabasesBackupsTests(test.TestCase): @@ -191,3 +199,63 @@ class DatabasesBackupsTests(test.TestCase): args=[incr_backup.id]) res = self.client.get(url) self.assertTemplateUsed(res, 'project/database_backups/details.html') + + @test.create_stubs({ + api.trove: ('backup_get', 'backup_list', 'configuration_list', + 'datastore_flavors', 'datastore_list', + 'datastore_version_list', 'instance_list'), + dash_api.cinder: ('volume_type_list',), + dash_api.neutron: ('network_list',), + dash_api.nova: ('availability_zone_list',), + policy: ('check',), + }) + def test_restore_backup(self): + policy.check((), IsA(http.HttpRequest)).MultipleTimes().AndReturn(True) + backup = self.database_backups.first() + api.trove.backup_get(IsA(http.HttpRequest), IsA(six.text_type)) \ + .AndReturn(self.database_backups.first()) + api.trove.backup_list(IsA(http.HttpRequest)).AndReturn( + self.database_backups.list()) + api.trove.configuration_list(IsA(http.HttpRequest)) \ + .AndReturn(self.database_configurations.list()) + api.trove.datastore_flavors(IsA(http.HttpRequest), + IsA(six.string_types), + IsA(six.string_types)) \ + .AndReturn(self.flavors.list()) + api.trove.datastore_list(IsA(http.HttpRequest)) \ + .AndReturn(self.datastores.list()) + api.trove.datastore_version_list(IsA(http.HttpRequest), + backup.datastore['type']) \ + .AndReturn(self.datastore_versions.list()) + api.trove.instance_list(IsA(http.HttpRequest), marker=None) \ + .AndReturn(common.Paginated(self.databases.list())) + dash_api.cinder.volume_type_list(IsA(http.HttpRequest)).AndReturn([]) + dash_api.neutron.network_list(IsA(http.HttpRequest), + tenant_id=self.tenant.id, + shared=False).\ + AndReturn(self.networks.list()[:1]) + dash_api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) + self.mox.ReplayAll() + + url = RESTORE_URL + '?backup=%s' % self.database_backups.first().id + res = self.client.get(url) + self.assertTemplateUsed(res, 'project/databases/launch.html') + + set_instance_detail_step = \ + [step for step in res.context_data['workflow'].steps + if isinstance(step, create_instance.SetInstanceDetails)][0] + fields = set_instance_detail_step.action.fields + self.assertTrue(len(fields['datastore'].choices), 1) + text = 'mysql - 5.6' + choice = fields['datastore'].choices[0] + self.assertTrue(choice[0], binascii.hexlify(text)) + self.assertTrue(choice[1], text) + + advanced_step = [step for step in res.context_data['workflow'].steps + if isinstance(step, create_instance.Advanced)][0] + fields = advanced_step.action.fields + self.assertTrue(len(fields['initial_state'].choices), 1) + choice = fields['initial_state'].choices[0] + self.assertTrue(choice[0], 'backup') + self.assertTrue(choice[1], _('Restore from Backup')) diff --git a/trove_dashboard/content/databases/workflows/create_instance.py b/trove_dashboard/content/databases/workflows/create_instance.py index 513d57a..30c58af 100644 --- a/trove_dashboard/content/databases/workflows/create_instance.py +++ b/trove_dashboard/content/databases/workflows/create_instance.py @@ -64,6 +64,11 @@ class SetInstanceDetailsAction(workflows.Action): })) def __init__(self, request, *args, **kwargs): + if args: + self.backup_id = args[0].get('backup', None) + else: + self.backup_id = None + super(SetInstanceDetailsAction, self).__init__(request, *args, **kwargs) @@ -184,11 +189,24 @@ class SetInstanceDetailsAction(workflows.Action): LOG.exception("Exception while obtaining datastore version list") self._datastore_versions = [] + @memoized.memoized_method + def get_backup(self, request, backup_id): + try: + return api.trove.backup_get(request, backup_id) + except Exception: + LOG.exception("Exception while obtaining backup information") + return None + def populate_datastore_choices(self, request, context): choices = () datastores = self.datastores(request) if datastores is not None: + if self.backup_id: + backup = self.get_backup(request, self.backup_id) for ds in datastores: + if self.backup_id: + if ds.name != backup.datastore['type']: + continue versions = self.datastore_versions(request, ds.name) if versions: # only add to choices if datastore has at least one version @@ -196,6 +214,9 @@ class SetInstanceDetailsAction(workflows.Action): for v in versions: if hasattr(v, 'active') and not v.active: continue + if self.backup_id: + if v.id != backup.datastore['version_id']: + continue selection_text = self._build_datastore_display_text( ds.name, v.name) widget_text = self._build_widget_field_name( @@ -350,6 +371,18 @@ class AdvancedAction(workflows.Action): 'data-initial_state-master': _('Replica Count') })) + def __init__(self, request, *args, **kwargs): + if args[0]: + self.backup_id = args[0].get('backup', None) + else: + self.backup_id = None + + super(AdvancedAction, self).__init__(request, *args, **kwargs) + + if self.backup_id: + self.fields['initial_state'].choices = [('backup', + _('Restore from Backup'))] + class Meta(object): name = _("Advanced") help_text_template = "project/databases/_launch_advanced_help.html" @@ -374,9 +407,13 @@ class AdvancedAction(workflows.Action): def populate_backup_choices(self, request, context): try: + choices = [] backups = api.trove.backup_list(request) - choices = [(b.id, b.name) for b in backups - if b.status == 'COMPLETED'] + for b in backups: + if self.backup_id and b.id != self.backup_id: + continue + if b.status == 'COMPLETED': + choices.append((b.id, b.name)) except Exception: choices = [] diff --git a/trove_dashboard/test/test_data/trove_data.py b/trove_dashboard/test/test_data/trove_data.py index dca3f76..1f0a4f6 100644 --- a/trove_dashboard/test/test_data/trove_data.py +++ b/trove_dashboard/test/test_data/trove_data.py @@ -229,6 +229,11 @@ BACKUP_ONE = { "size": 0.13, "id": "0edb3c14-8919-4583-9add-00df9e524081", "description": "Long description of backup", + "datastore": { + "type": "mysql", + "version": "5.6", + "version_id": "500a6d52-8347-4e00-8e4c-f4fa9cf96ae9" + }, } BACKUP_TWO = { @@ -241,6 +246,11 @@ BACKUP_TWO = { "size": 0.13, "id": "e4602a3c-2bca-478f-b059-b6c215510fb4", "description": "Longer description of backup", + "datastore": { + "type": "mysql", + "version": "5.6", + "version_id": "500a6d52-8347-4e00-8e4c-f4fa9cf96ae9" + }, } BACKUP_TWO_INC = { @@ -254,6 +264,11 @@ BACKUP_TWO_INC = { "id": "e4602a3c-2bca-478f-b059-b6c215510fb5", "description": "Longer description of backup", "parent_id": "e4602a3c-2bca-478f-b059-b6c215510fb4", + "datastore": { + "type": "mysql", + "version": "5.6", + "version_id": "500a6d52-8347-4e00-8e4c-f4fa9cf96ae9" + }, } CONFIG_ONE = {