diff --git a/releasenotes/notes/dashboard-configuration-id-afd3b51347d54c00.yaml b/releasenotes/notes/dashboard-configuration-id-afd3b51347d54c00.yaml new file mode 100644 index 0000000..48d5ec3 --- /dev/null +++ b/releasenotes/notes/dashboard-configuration-id-afd3b51347d54c00.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Enables dashboard to select a configuration-id. + We should select a configuration-id in cluster creation forms because we + can select a configuration id when creating a database instance. diff --git a/trove_dashboard/api/trove.py b/trove_dashboard/api/trove.py index d3a0561..80c8773 100644 --- a/trove_dashboard/api/trove.py +++ b/trove_dashboard/api/trove.py @@ -69,7 +69,8 @@ def cluster_delete(request, cluster_id): def cluster_create(request, name, volume, flavor, num_instances, datastore, datastore_version, - nics=None, root_password=None, locality=None): + nics=None, root_password=None, locality=None, + configuration=None): instances = [] for i in range(num_instances): instance = {} @@ -86,7 +87,8 @@ def cluster_create(request, name, volume, flavor, num_instances, datastore, datastore_version, instances=instances, - locality=locality) + locality=locality, + configuration=configuration) def cluster_grow(request, cluster_id, new_instances): diff --git a/trove_dashboard/content/database_clusters/forms.py b/trove_dashboard/content/database_clusters/forms.py index b3ff1f6..f38f4cf 100644 --- a/trove_dashboard/content/database_clusters/forms.py +++ b/trove_dashboard/content/database_clusters/forms.py @@ -40,7 +40,51 @@ from trove_dashboard.utils import common as common_utils LOG = logging.getLogger(__name__) -class LaunchForm(forms.SelfHandlingForm): +class BaseClusterForm(forms.SelfHandlingForm): + def __init__(self, request, *args, **kwargs): + super(BaseClusterForm, self).__init__(request, *args, **kwargs) + + @memoized.memoized_method + def populate_network_choices(self, request): + network_list = [] + try: + if api.base.is_service_enabled(request, 'network'): + tenant_id = self.request.user.tenant_id + networks = api.neutron.network_list_for_tenant(request, + tenant_id) + network_list = [(network.id, network.name_or_id) + for network in networks] + else: + self.fields['network'].widget = forms.HiddenInput() + except exceptions.ServiceCatalogException: + network_list = [] + redirect = reverse('horizon:project:database_clusters:index') + exceptions.handle(request, + _('Unable to retrieve networks.'), + redirect=redirect) + return network_list + + @memoized.memoized_method + def populate_cg_choices(self, request): + try: + configs = trove_api.trove.configuration_list(request) + config_name = "%(name)s (%(datastore)s - %(version)s)" + choices = [(c.id, + config_name % {'name': c.name, + 'datastore': c.datastore_name, + 'version': c.datastore_version_name}) + for c in configs] + except Exception: + choices = [] + + if choices: + choices.insert(0, ("", _("Select configuration"))) + else: + choices.insert(0, ("", _("No configurations available"))) + return choices + + +class LaunchForm(BaseClusterForm): name = forms.CharField(label=_("Cluster Name"), max_length=80) datastore = forms.ChoiceField( @@ -68,6 +112,10 @@ class LaunchForm(forms.SelfHandlingForm): help_text=_("Specify whether instances in the cluster will " "be created on the same hypervisor (affinity) or on " "different hypervisors (anti-affinity).")) + configuration = forms.ChoiceField( + label=_("Configuration Group"), + help_text=_("Configuration Group attached to instances."), + required=False) root_password = forms.CharField( label=_("Root Password"), required=False, @@ -81,7 +129,7 @@ class LaunchForm(forms.SelfHandlingForm): min_value=3, initial=3, required=False, - help_text=_("Number of instances in the cluster. (Read only)"), + help_text=_("Number of instances in the cluster (read only)."), widget=forms.TextInput(attrs={ 'readonly': 'readonly', 'class': 'switched', @@ -92,7 +140,7 @@ class LaunchForm(forms.SelfHandlingForm): min_value=1, initial=1, required=False, - help_text=_("Number of shards. (Read only)"), + help_text=_("Number of shards (read only)."), widget=forms.TextInput(attrs={ 'readonly': 'readonly', 'class': 'switched', @@ -127,6 +175,8 @@ class LaunchForm(forms.SelfHandlingForm): request) self.fields['network'].choices = self.populate_network_choices( request) + self.fields['configuration'].choices = self.populate_cg_choices( + request) def clean(self): datastore_field_value = self.data.get("datastore", None) @@ -158,6 +208,9 @@ class LaunchForm(forms.SelfHandlingForm): if not self.data.get("locality", None): self.cleaned_data["locality"] = None + if not self.data.get("configuration", None): + self.cleaned_data["configuration"] = None + return self.cleaned_data @memoized.memoized_method @@ -173,26 +226,6 @@ class LaunchForm(forms.SelfHandlingForm): _('Unable to obtain flavors.'), redirect=redirect) - @memoized.memoized_method - def populate_network_choices(self, request): - network_list = [] - try: - if api.base.is_service_enabled(request, 'network'): - tenant_id = self.request.user.tenant_id - networks = api.neutron.network_list_for_tenant(request, - tenant_id) - network_list = [(network.id, network.name_or_id) - for network in networks] - else: - self.fields['network'].widget = forms.HiddenInput() - except exceptions.ServiceCatalogException: - network_list = [] - redirect = reverse('horizon:project:database_clusters:index') - exceptions.handle(request, - _('Unable to retrieve networks.'), - redirect=redirect) - return network_list - @memoized.memoized_method def datastores(self, request): try: @@ -354,9 +387,10 @@ class LaunchForm(forms.SelfHandlingForm): LOG.info("Launching cluster with parameters " "{name=%s, volume=%s, flavor=%s, " "datastore=%s, datastore_version=%s", - "locality=%s", + "locality=%s, configuration=%s", data['name'], data['volume'], flavor, - datastore, datastore_version, self._get_locality(data)) + datastore, datastore_version, self._get_locality(data), + configuration=data['configuration']) trove_api.trove.cluster_create(request, data['name'], @@ -367,7 +401,8 @@ class LaunchForm(forms.SelfHandlingForm): datastore_version=datastore_version, nics=data['network'], root_password=root_password, - locality=self._get_locality(data)) + locality=self._get_locality(data), + configuration=data['configuration']) messages.success(request, _('Launched cluster "%s"') % data['name']) return True @@ -378,7 +413,7 @@ class LaunchForm(forms.SelfHandlingForm): redirect=redirect) -class ClusterAddInstanceForm(forms.SelfHandlingForm): +class ClusterAddInstanceForm(BaseClusterForm): cluster_id = forms.CharField( required=False, widget=forms.HiddenInput()) @@ -408,6 +443,10 @@ class ClusterAddInstanceForm(forms.SelfHandlingForm): label=_("Network"), help_text=_("Network attached to instance."), required=False) + configuration = forms.ChoiceField( + label=_("Configuration Group"), + help_text=_("Configuration Group attached to instance."), + required=False) def __init__(self, request, *args, **kwargs): super(ClusterAddInstanceForm, self).__init__(request, *args, **kwargs) @@ -415,6 +454,8 @@ class ClusterAddInstanceForm(forms.SelfHandlingForm): self.fields['flavor'].choices = self.populate_flavor_choices(request) self.fields['network'].choices = self.populate_network_choices( request) + self.fields['configuration'].choices = self.populate_cg_choices( + request) @memoized.memoized_method def flavors(self, request): @@ -441,26 +482,6 @@ class ClusterAddInstanceForm(forms.SelfHandlingForm): flavor_list = [(f.id, "%s" % f.name) for f in self.flavors(request)] return sorted(flavor_list) - @memoized.memoized_method - def populate_network_choices(self, request): - network_list = [] - try: - if api.base.is_service_enabled(request, 'network'): - tenant_id = self.request.user.tenant_id - networks = api.neutron.network_list_for_tenant(request, - tenant_id) - network_list = [(network.id, network.name_or_id) - for network in networks] - else: - self.fields['network'].widget = forms.HiddenInput() - except exceptions.ServiceCatalogException: - network_list = [] - redirect = reverse('horizon:project:database_clusters:index') - exceptions.handle(request, - _('Unable to retrieve networks.'), - redirect=redirect) - return network_list - def handle(self, request, data): try: flavor = trove_api.trove.flavor_get(request, data['flavor']) diff --git a/trove_dashboard/content/database_clusters/panel.py b/trove_dashboard/content/database_clusters/panel.py index d2a2148..7caaace 100644 --- a/trove_dashboard/content/database_clusters/panel.py +++ b/trove_dashboard/content/database_clusters/panel.py @@ -17,6 +17,7 @@ from django.utils.translation import gettext_lazy as _ import horizon +from openstack_dashboard.dashboards.project import dashboard class Clusters(horizon.Panel): @@ -25,4 +26,4 @@ class Clusters(horizon.Panel): permissions = ('openstack.services.database',) -# dashboard.Project.register(Clusters) +dashboard.Project.register(Clusters) diff --git a/trove_dashboard/content/database_clusters/tests.py b/trove_dashboard/content/database_clusters/tests.py new file mode 100644 index 0000000..1c9e7f5 --- /dev/null +++ b/trove_dashboard/content/database_clusters/tests.py @@ -0,0 +1,682 @@ +# Copyright (c) 2014 eBay Software Foundation +# Copyright 2015 HP Software, LLC +# All Rights Reserved. +# +# 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 logging +from unittest import mock + +from django.urls import reverse +from openstack_dashboard import api +from troveclient import common + +from trove_dashboard import api as trove_api +from trove_dashboard.content.database_clusters \ + import cluster_manager +from trove_dashboard.content.database_clusters import tables +from trove_dashboard.test import helpers as test +from trove_dashboard.utils import common as common_utils + +INDEX_URL = reverse('horizon:project:database_clusters:index') +LAUNCH_URL = reverse('horizon:project:database_clusters:launch') +DETAILS_URL = reverse('horizon:project:database_clusters:detail', args=['id']) +RESET_PASSWORD_VIEWNAME = 'horizon:project:database_clusters:reset_password' + + +class ClustersTests(test.TestCase): + @test.create_mocks({trove_api.trove: ('cluster_list', + 'flavor_list')}) + def test_index(self): + clusters = common.Paginated(self.trove_clusters.list()) + self.mock_cluster_list.return_value = clusters + self.mock_flavor_list.return_value = self.flavors.list() + + res = self.client.get(INDEX_URL) + self.mock_cluster_list.assert_called_once_with( + test.IsHttpRequest(), marker=None) + self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest()) + self.assertTemplateUsed(res, 'project/database_clusters/index.html') + + @test.create_mocks({trove_api.trove: ('cluster_list', + 'flavor_list')}) + def test_index_flavor_exception(self): + clusters = common.Paginated(self.trove_clusters.list()) + self.mock_cluster_list.return_value = clusters + self.mock_flavor_list.side_effect = self.exceptions.trove + + res = self.client.get(INDEX_URL) + self.mock_cluster_list.assert_called_once_with( + test.IsHttpRequest(), marker=None) + self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest()) + self.assertTemplateUsed(res, 'project/database_clusters/index.html') + self.assertMessageCount(res, error=1) + + @test.create_mocks({trove_api.trove: ('cluster_list',)}) + def test_index_list_exception(self): + self.mock_cluster_list.side_effect = self.exceptions.trove + + res = self.client.get(INDEX_URL) + self.mock_cluster_list.assert_called_once_with( + test.IsHttpRequest(), marker=None) + self.assertTemplateUsed(res, 'project/database_clusters/index.html') + self.assertMessageCount(res, error=1) + + @test.create_mocks({trove_api.trove: ('cluster_list', + 'flavor_list')}) + def test_index_pagination(self): + clusters = self.trove_clusters.list() + last_record = clusters[1] + clusters = common.Paginated(clusters, next_marker="foo") + self.mock_cluster_list.return_value = clusters + self.mock_flavor_list.return_value = self.flavors.list() + + res = self.client.get(INDEX_URL) + self.mock_cluster_list.assert_called_once_with( + test.IsHttpRequest(), marker=None) + self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest()) + self.assertTemplateUsed(res, 'project/database_clusters/index.html') + self.assertContains( + res, 'marker=' + last_record.id) + + @test.create_mocks({trove_api.trove: ('datastore_flavors', + 'datastore_list', + 'datastore_version_list'), + api.base: ['is_service_enabled']}) + def test_launch_cluster(self): + self.mock_is_service_enabled.return_value = False + self.mock_datastore_flavors.return_value = self.flavors.list() + + filtered_datastores = self._get_filtered_datastores('mongodb') + self.mock_datastore_list.return_value = filtered_datastores + self.mock_datastore_version_list.return_value = ( + self._get_filtered_datastore_versions(filtered_datastores)) + + res = self.client.get(LAUNCH_URL) + self.mock_is_service_enabled.assert_called_once_with( + test.IsHttpRequest(), 'network') + self.mock_datastore_flavors.assert_called_once_with( + test.IsHttpRequest(), 'mongodb', '2.6') + self.mock_datastore_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_datastore_version_list.assert_called_once_with( + test.IsHttpRequest(), test.IsA(str)) + self.assertTemplateUsed(res, 'project/database_clusters/launch.html') + + def test_launch_cluster_mongo_fields(self): + datastore = 'mongodb' + datastore_version = '2.6' + fields = self.launch_cluster_fields_setup(datastore, + datastore_version) + field_name = self._build_flavor_widget_name(datastore, + datastore_version) + + self.assertTrue(self._contains_datastore_in_attribute( + fields[field_name], field_name)) + self.assertTrue(self._contains_datastore_in_attribute( + fields['num_instances'], field_name)) + self.assertTrue(self._contains_datastore_in_attribute( + fields['num_shards'], field_name)) + self.assertFalse(self._contains_datastore_in_attribute( + fields['root_password'], field_name)) + self.assertFalse(self._contains_datastore_in_attribute( + fields['num_instances_vertica'], field_name)) + + def test_launch_cluster_redis_fields(self): + datastore = 'redis' + datastore_version = '3.0' + fields = self.launch_cluster_fields_setup(datastore, + datastore_version) + field_name = self._build_flavor_widget_name(datastore, + datastore_version) + + self.assertTrue(self._contains_datastore_in_attribute( + fields[field_name], field_name)) + self.assertTrue(self._contains_datastore_in_attribute( + fields['num_instances'], field_name)) + self.assertFalse(self._contains_datastore_in_attribute( + fields['num_shards'], field_name)) + self.assertFalse(self._contains_datastore_in_attribute( + fields['root_password'], field_name)) + self.assertFalse(self._contains_datastore_in_attribute( + fields['num_instances_vertica'], field_name)) + + def test_launch_cluster_vertica_fields(self): + datastore = 'vertica' + datastore_version = '7.1' + fields = self.launch_cluster_fields_setup(datastore, + datastore_version) + field_name = self._build_flavor_widget_name(datastore, + datastore_version) + + self.assertTrue(self._contains_datastore_in_attribute( + fields[field_name], field_name)) + self.assertFalse(self._contains_datastore_in_attribute( + fields['num_instances'], field_name)) + self.assertFalse(self._contains_datastore_in_attribute( + fields['num_shards'], field_name)) + self.assertTrue(self._contains_datastore_in_attribute( + fields['root_password'], field_name)) + self.assertTrue(self._contains_datastore_in_attribute( + fields['num_instances_vertica'], field_name)) + + @test.create_mocks({trove_api.trove: ('datastore_flavors', + 'datastore_list', + 'datastore_version_list'), + api.base: ['is_service_enabled']}) + def launch_cluster_fields_setup(self, datastore, datastore_version): + self.mock_is_service_enabled.return_value = False + self.mock_datastore_flavors.return_value = self.flavors.list() + + filtered_datastores = self._get_filtered_datastores(datastore) + self.mock_datastore_list.return_value = filtered_datastores + self.mock_datastore_version_list.return_value = ( + self._get_filtered_datastore_versions(filtered_datastores)) + + res = self.client.get(LAUNCH_URL) + self.mock_is_service_enabled.assert_called_once_with( + test.IsHttpRequest(), 'network') + self.mock_datastore_flavors.assert_called_once_with( + test.IsHttpRequest(), datastore, datastore_version) + self.mock_datastore_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_datastore_version_list.assert_called_once_with( + test.IsHttpRequest(), test.IsA(str)) + return res.context_data['form'].fields + + @test.create_mocks({trove_api.trove: ['datastore_flavors', + 'cluster_create', + 'datastore_list', + 'datastore_version_list'], + api.base: ['is_service_enabled']}) + def test_create_simple_cluster(self): + self.mock_is_service_enabled.return_value = False + self.mock_datastore_flavors.return_value = self.flavors.list() + + filtered_datastores = self._get_filtered_datastores('mongodb') + self.mock_datastore_list.return_value = filtered_datastores + self.mock_datastore_version_list.return_value = ( + self._get_filtered_datastore_versions(filtered_datastores)) + + self.mock_cluster_create.return_value = self.trove_clusters.first() + + cluster_name = u'MyCluster' + cluster_volume = 1 + cluster_flavor = u'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + cluster_instances = 3 + cluster_datastore = u'mongodb' + cluster_datastore_version = u'2.6' + cluster_network = u'' + + field_name = self._build_flavor_widget_name(cluster_datastore, + cluster_datastore_version) + post = { + 'name': cluster_name, + 'volume': cluster_volume, + 'num_instances': cluster_instances, + 'num_shards': 1, + 'datastore': field_name, + field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + } + + res = self.client.post(LAUNCH_URL, post) + self.mock_is_service_enabled.assert_called_once_with( + test.IsHttpRequest(), 'network') + self.mock_datastore_flavors.assert_called_once_with( + test.IsHttpRequest(), 'mongodb', '2.6') + self.mock_datastore_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_datastore_version_list.assert_called_once_with( + test.IsHttpRequest(), test.IsA(str)) + self.mock_cluster_create.assert_called_once_with( + test.IsHttpRequest(), + cluster_name, + cluster_volume, + cluster_flavor, + cluster_instances, + datastore=cluster_datastore, + datastore_version=cluster_datastore_version, + nics=cluster_network, + root_password=None, + locality=None, + configuration=None) + self.assertNoFormErrors(res) + self.assertMessageCount(success=1) + + @test.create_mocks({trove_api.trove: ['datastore_flavors', + 'cluster_create', + 'datastore_list', + 'datastore_version_list'], + api.neutron: ['network_list_for_tenant'], + api.base: ['is_service_enabled']}) + def test_create_simple_cluster_neutron(self): + self.mock_is_service_enabled.return_value = True + self.mock_network_list_for_tenant.return_value = self.networks.list() + self.mock_datastore_flavors.return_value = self.flavors.list() + + filtered_datastores = self._get_filtered_datastores('mongodb') + self.mock_datastore_list.return_value = filtered_datastores + self.mock_datastore_version_list.return_value = ( + self._get_filtered_datastore_versions(filtered_datastores)) + + self.mock_cluster_create.return_value = self.trove_clusters.first() + + cluster_name = u'MyCluster' + cluster_volume = 1 + cluster_flavor = u'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + cluster_instances = 3 + cluster_datastore = u'mongodb' + cluster_datastore_version = u'2.6' + cluster_network = u'82288d84-e0a5-42ac-95be-e6af08727e42' + + field_name = self._build_flavor_widget_name(cluster_datastore, + cluster_datastore_version) + post = { + 'name': cluster_name, + 'volume': cluster_volume, + 'num_instances': cluster_instances, + 'num_shards': 1, + 'datastore': field_name, + field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'network': cluster_network, + } + + res = self.client.post(LAUNCH_URL, post) + self.mock_is_service_enabled.assert_called_once_with( + test.IsHttpRequest(), 'network') + self.mock_network_list_for_tenant.assert_called_once_with( + test.IsHttpRequest(), '1') + self.mock_datastore_flavors.assert_called_once_with( + test.IsHttpRequest(), 'mongodb', '2.6') + self.mock_datastore_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_datastore_version_list.assert_called_once_with( + test.IsHttpRequest(), test.IsA(str)) + self.mock_cluster_create.assert_called_once_with( + test.IsHttpRequest(), + cluster_name, + cluster_volume, + cluster_flavor, + cluster_instances, + datastore=cluster_datastore, + datastore_version=cluster_datastore_version, + nics=cluster_network, + root_password=None, + locality=None, + configuration=None) + self.assertNoFormErrors(res) + self.assertMessageCount(success=1) + + @test.create_mocks({trove_api.trove: ['datastore_flavors', + 'cluster_create', + 'datastore_list', + 'datastore_version_list'], + api.neutron: ['network_list_for_tenant']}) + def test_create_simple_cluster_exception(self): + self.mock_network_list_for_tenant.return_value = self.networks.list() + self.mock_datastore_flavors.return_value = self.flavors.list() + + filtered_datastores = self._get_filtered_datastores('mongodb') + self.mock_datastore_list.return_value = filtered_datastores + self.mock_datastore_version_list.return_value = ( + self._get_filtered_datastore_versions(filtered_datastores)) + + self.mock_cluster_create.side_effect = self.exceptions.trove + + cluster_name = u'MyCluster' + cluster_volume = 1 + cluster_flavor = u'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + cluster_instances = 3 + cluster_datastore = u'mongodb' + cluster_datastore_version = u'2.6' + cluster_network = u'' + + field_name = self._build_flavor_widget_name(cluster_datastore, + cluster_datastore_version) + post = { + 'name': cluster_name, + 'volume': cluster_volume, + 'num_instances': cluster_instances, + 'num_shards': 1, + 'datastore': field_name, + field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + } + + res = self.client.post(LAUNCH_URL, post) + self.mock_network_list_for_tenant.assert_called_once_with( + test.IsHttpRequest(), '1') + self.mock_datastore_flavors.assert_called_once_with( + test.IsHttpRequest(), 'mongodb', '2.6') + self.mock_datastore_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_datastore_version_list.assert_called_once_with( + test.IsHttpRequest(), test.IsA(str)) + self.mock_cluster_create.assert_called_once_with( + test.IsHttpRequest(), + cluster_name, + cluster_volume, + cluster_flavor, + cluster_instances, + datastore=cluster_datastore, + datastore_version=cluster_datastore_version, + nics=cluster_network, + root_password=None, + locality=None, + configuration=None) + self.assertRedirectsNoFollow(res, INDEX_URL) + self.assertMessageCount(error=1) + + @test.create_mocks({trove_api.trove: ('cluster_get', + 'instance_get', + 'flavor_get',)}) + def test_details(self): + cluster = self.trove_clusters.first() + self.mock_cluster_get.return_value = cluster + self.mock_instance_get.return_value = self.databases.first() + self.mock_flavor_get.return_value = self.flavors.first() + + details_url = reverse('horizon:project:database_clusters:detail', + args=[cluster.id]) + res = self.client.get(details_url) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_cluster_get, 2, + mock.call(test.IsHttpRequest(), cluster.id)) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_instance_get, 3, + mock.call(test.IsHttpRequest(), test.IsA(str))) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_flavor_get, 4, + mock.call(test.IsHttpRequest(), test.IsA(str))) + self.assertTemplateUsed(res, 'horizon/common/_detail.html') + self.assertContains(res, cluster.ip[0]) + + @test.create_mocks({trove_api.trove: ('cluster_get', + 'instance_get', + 'flavor_get',)}) + def test_details_without_locality(self): + cluster = self.trove_clusters.list()[1] + self.mock_cluster_get.return_value = cluster + self.mock_instance_get.return_value = self.databases.first() + self.mock_flavor_get.return_value = self.flavors.first() + + details_url = reverse('horizon:project:database_clusters:detail', + args=[cluster.id]) + res = self.client.get(details_url) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_cluster_get, 2, + mock.call(test.IsHttpRequest(), cluster.id)) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_instance_get, 3, + mock.call(test.IsHttpRequest(), test.IsA(str))) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_flavor_get, 4, + mock.call(test.IsHttpRequest(), test.IsA(str))) + self.assertTemplateUsed(res, 'horizon/common/_detail.html') + self.assertNotContains(res, "Locality") + + @test.create_mocks({trove_api.trove: ('cluster_get', + 'instance_get', + 'flavor_get',)}) + def test_details_with_locality(self): + cluster = self.trove_clusters.first() + self.mock_cluster_get.return_value = cluster + self.mock_instance_get.return_value = self.databases.first() + self.mock_flavor_get.return_value = self.flavors.first() + + details_url = reverse('horizon:project:database_clusters:detail', + args=[cluster.id]) + res = self.client.get(details_url) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_cluster_get, 2, + mock.call(test.IsHttpRequest(), cluster.id)) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_instance_get, 3, + mock.call(test.IsHttpRequest(), test.IsA(str))) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_flavor_get, 4, + mock.call(test.IsHttpRequest(), test.IsA(str))) + self.assertTemplateUsed(res, 'project/database_clusters/' + '_detail_overview.html') + self.assertContains(res, "Locality") + + @test.create_mocks( + {trove_api.trove: ('cluster_get', + 'cluster_grow'), + cluster_manager: ('get',)}) + def test_grow_cluster(self): + cluster = self.trove_clusters.first() + self.mock_cluster_get.return_value = cluster + cluster_volume = 1 + flavor = self.flavors.first() + cluster_flavor = flavor.id + cluster_flavor_name = flavor.name + instances = [ + cluster_manager.ClusterInstance("id1", "name1", cluster_flavor, + cluster_flavor_name, + cluster_volume, "master", None, + None), + cluster_manager.ClusterInstance("id2", "name2", cluster_flavor, + cluster_flavor_name, + cluster_volume, "slave", + "master", None), + cluster_manager.ClusterInstance("id3", None, cluster_flavor, + cluster_flavor_name, + cluster_volume, None, None, None), + ] + + manager = cluster_manager.ClusterInstanceManager(cluster.id) + manager.instances = instances + self.mock_get.return_value = manager + + url = reverse('horizon:project:database_clusters:cluster_grow_details', + args=[cluster.id]) + res = self.client.get(url) + self.assertTemplateUsed( + res, 'project/database_clusters/cluster_grow_details.html') + table = res.context_data[ + "".join([tables.ClusterGrowInstancesTable.Meta.name, '_table'])] + self.assertEqual(len(cluster.instances), len(table.data)) + + action = "".join([tables.ClusterGrowInstancesTable.Meta.name, '__', + tables.ClusterGrowRemoveInstance.name, '__', + 'id1']) + self.client.post(url, {'action': action}) + self.assertEqual(len(cluster.instances) - 1, len(table.data)) + + action = "".join([tables.ClusterGrowInstancesTable.Meta.name, '__', + tables.ClusterGrowAction.name, '__', + cluster.id]) + res = self.client.post(url, {'action': action}) + self.mock_cluster_get.assert_called_once_with( + test.IsHttpRequest(), cluster.id) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_get, 5, mock.call(cluster.id)) + self.mock_cluster_grow.assert_called_once_with( + test.IsHttpRequest(), cluster.id, instances) + self.assertMessageCount(success=1) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_mocks({trove_api.trove: ('cluster_get',)}) + def test_grow_cluster_no_instances(self): + cluster = self.trove_clusters.first() + self.mock_cluster_get.return_value = cluster + + url = reverse('horizon:project:database_clusters:cluster_grow_details', + args=[cluster.id]) + res = self.client.get(url) + self.assertTemplateUsed( + res, 'project/database_clusters/cluster_grow_details.html') + + action = "".join([tables.ClusterGrowInstancesTable.Meta.name, '__', + tables.ClusterGrowAction.name, '__', + cluster.id]) + self.client.post(url, {'action': action}) + self.mock_cluster_get.assert_called_once_with( + test.IsHttpRequest(), cluster.id) + self.assertMessageCount(info=1) + + @test.create_mocks( + {trove_api.trove: ('cluster_get', + 'cluster_grow',), + cluster_manager: ('get',)}) + def test_grow_cluster_exception(self): + cluster = self.trove_clusters.first() + self.mock_cluster_get.return_value = cluster + cluster_volume = 1 + flavor = self.flavors.first() + cluster_flavor = flavor.id + cluster_flavor_name = flavor.name + instances = [ + cluster_manager.ClusterInstance("id1", "name1", cluster_flavor, + cluster_flavor_name, + cluster_volume, "master", None, + None), + cluster_manager.ClusterInstance("id2", "name2", cluster_flavor, + cluster_flavor_name, + cluster_volume, "slave", + "master", None), + cluster_manager.ClusterInstance("id3", None, cluster_flavor, + cluster_flavor_name, + cluster_volume, None, None, None), + ] + + manager = cluster_manager.ClusterInstanceManager(cluster.id) + manager.instances = instances + self.mock_get.return_value = manager + self.mock_cluster_grow.side_effect = self.exceptions.trove + + url = reverse('horizon:project:database_clusters:cluster_grow_details', + args=[cluster.id]) + res = self.client.get(url) + self.assertTemplateUsed( + res, 'project/database_clusters/cluster_grow_details.html') + + toSuppress = ["trove_dashboard.content.database_clusters.tables"] + + # Suppress expected log messages in the test output + loggers = [] + for cls in toSuppress: + logger = logging.getLogger(cls) + loggers.append((logger, logger.getEffectiveLevel())) + logger.setLevel(logging.CRITICAL) + + try: + action = "".join([tables.ClusterGrowInstancesTable.Meta.name, '__', + tables.ClusterGrowAction.name, '__', + cluster.id]) + res = self.client.post(url, {'action': action}) + + self.mock_cluster_get.assert_called_once_with( + test.IsHttpRequest(), cluster.id) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_get, 3, mock.call(cluster.id)) + self.mock_cluster_grow.assert_called_once_with( + test.IsHttpRequest(), cluster.id, instances) + self.assertMessageCount(error=1) + self.assertRedirectsNoFollow(res, INDEX_URL) + finally: + # Restore the previous log levels + for (log, level) in loggers: + log.setLevel(level) + + @test.create_mocks({trove_api.trove: ('cluster_get', + 'cluster_shrink')}) + def test_shrink_cluster(self): + cluster = self.trove_clusters.first() + self.mock_cluster_get.return_value = cluster + instance_id = cluster.instances[0]['id'] + cluster_instances = [{'id': instance_id}] + + url = reverse( + 'horizon:project:database_clusters:cluster_shrink_details', + args=[cluster.id]) + res = self.client.get(url) + self.assertTemplateUsed( + res, 'project/database_clusters/cluster_shrink_details.html') + table = res.context_data[ + "".join([tables.ClusterShrinkInstancesTable.Meta.name, '_table'])] + self.assertEqual(len(cluster.instances), len(table.data)) + + action = "".join([tables.ClusterShrinkInstancesTable.Meta.name, '__', + tables.ClusterShrinkAction.name, '__', + instance_id]) + res = self.client.post(url, {'action': action}) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_cluster_get, 2, + mock.call(test.IsHttpRequest(), cluster.id)) + self.mock_cluster_shrink.assert_called_once_with( + test.IsHttpRequest(), cluster.id, cluster_instances) + self.assertNoFormErrors(res) + self.assertMessageCount(info=1) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_mocks({trove_api.trove: ('cluster_get', + 'cluster_shrink')}) + def test_shrink_cluster_exception(self): + cluster = self.trove_clusters.first() + self.mock_cluster_get.return_value = cluster + instance_id = cluster.instances[0]['id'] + cluster_instances = [{'id': instance_id}] + self.mock_cluster_shrink.side_effect = self.exceptions.trove + + url = reverse( + 'horizon:project:database_clusters:cluster_shrink_details', + args=[cluster.id]) + action = "".join([tables.ClusterShrinkInstancesTable.Meta.name, '__', + tables.ClusterShrinkAction.name, '__', + instance_id]) + + toSuppress = ["trove_dashboard.content.database_clusters.tables"] + + # Suppress expected log messages in the test output + loggers = [] + for cls in toSuppress: + logger = logging.getLogger(cls) + loggers.append((logger, logger.getEffectiveLevel())) + logger.setLevel(logging.CRITICAL) + + try: + res = self.client.post(url, {'action': action}) + self.mock_cluster_get.assert_called_once_with( + test.IsHttpRequest(), cluster.id) + self.mock_cluster_shrink.assert_called_once_with( + test.IsHttpRequest(), cluster.id, cluster_instances) + self.assertMessageCount(error=1) + self.assertRedirectsNoFollow(res, INDEX_URL) + finally: + # Restore the previous log levels + for (log, level) in loggers: + log.setLevel(level) + + def _get_filtered_datastores(self, datastore): + filtered_datastore = [] + for ds in self.datastores.list(): + if datastore in ds.name: + filtered_datastore.append(ds) + return filtered_datastore + + def _get_filtered_datastore_versions(self, datastores): + filtered_datastore_versions = [] + for ds in datastores: + for dsv in self.datastore_versions.list(): + if ds.id == dsv.datastore: + filtered_datastore_versions.append(dsv) + return filtered_datastore_versions + + def _contains_datastore_in_attribute(self, field, datastore): + for key, value in field.widget.attrs.items(): + if datastore in key: + return True + return False + + def _build_datastore_display_text(self, datastore, datastore_version): + return datastore + ' - ' + datastore_version + + def _build_flavor_widget_name(self, datastore, datastore_version): + return common_utils.hexlify(self._build_datastore_display_text( + datastore, datastore_version)) diff --git a/trove_dashboard/enabled/_1740_project_database_clusters_panel.py b/trove_dashboard/enabled/_1740_project_database_clusters_panel.py index cb61e8a..25efefd 100644 --- a/trove_dashboard/enabled/_1740_project_database_clusters_panel.py +++ b/trove_dashboard/enabled/_1740_project_database_clusters_panel.py @@ -22,7 +22,7 @@ PANEL_DASHBOARD = 'project' # The slug of the panel group the PANEL is associated with. PANEL_GROUP = 'database' -DISABLED = True +DISABLED = False # Python panel class of the PANEL to be added. ADD_PANEL = 'trove_dashboard.content.database_clusters.panel.Clusters'