diff --git a/openstack_dashboard/contrib/trove/api/trove.py b/openstack_dashboard/contrib/trove/api/trove.py
index e017081918..e55d9b1c1a 100644
--- a/openstack_dashboard/contrib/trove/api/trove.py
+++ b/openstack_dashboard/contrib/trove/api/trove.py
@@ -42,6 +42,57 @@ def troveclient(request):
return c
+def cluster_list(request, marker=None):
+ page_size = utils.get_page_size(request)
+ return troveclient(request).clusters.list(limit=page_size, marker=marker)
+
+
+def cluster_get(request, cluster_id):
+ return troveclient(request).clusters.get(cluster_id)
+
+
+def cluster_delete(request, cluster_id):
+ return troveclient(request).clusters.delete(cluster_id)
+
+
+def cluster_create(request, name, volume, flavor, num_instances,
+ datastore, datastore_version,
+ nics=None, root_password=None):
+ # TODO(dklyle): adding to support trove without volume
+ # support for now until API supports checking for volume support
+ if volume > 0:
+ volume_params = {'size': volume}
+ else:
+ volume_params = None
+ instances = []
+ for i in range(num_instances):
+ instance = {}
+ instance["flavorRef"] = flavor
+ instance["volume"] = volume_params
+ if nics:
+ instance["nics"] = [{"net-id": nics}]
+ instances.append(instance)
+
+ # TODO(saurabhs): vertica needs root password on cluster create
+ return troveclient(request).clusters.create(
+ name,
+ datastore,
+ datastore_version,
+ instances=instances)
+
+
+def cluster_add_shard(request, cluster_id):
+ return troveclient(request).clusters.add_shard(cluster_id)
+
+
+def create_cluster_root(request, cluster_id, password):
+ # It appears the code below depends on this trove change
+ # https://review.openstack.org/#/c/166954/. Comment out when that
+ # change merges.
+ # return troveclient(request).cluster.reset_root_password(cluster_id)
+ troveclient(request).root.create_cluster_root(cluster_id, password)
+
+
def instance_list(request, marker=None):
page_size = utils.get_page_size(request)
return troveclient(request).instances.list(limit=page_size, marker=marker)
@@ -130,6 +181,19 @@ def flavor_list(request):
return troveclient(request).flavors.list()
+def datastore_flavors(request, datastore_name=None,
+ datastore_version=None):
+ # if datastore info is available then get datastore specific flavors
+ if datastore_name and datastore_version:
+ try:
+ return troveclient(request).flavors.\
+ list_datastore_version_associated_flavors(datastore_name,
+ datastore_version)
+ except Exception:
+ LOG.warn("Failed to retrieve datastore specific flavors")
+ return flavor_list(request)
+
+
def flavor_get(request, flavor_id):
return troveclient(request).flavors.get(flavor_id)
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/__init__.py b/openstack_dashboard/contrib/trove/content/database_clusters/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/forms.py b/openstack_dashboard/contrib/trove/content/database_clusters/forms.py
new file mode 100644
index 0000000000..a08a84602a
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/forms.py
@@ -0,0 +1,375 @@
+# 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 django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+from django.views.decorators.debug import sensitive_variables # noqa
+
+from horizon import exceptions
+from horizon import forms
+from horizon import messages
+from horizon.utils import memoized
+
+from openstack_dashboard import api
+from openstack_dashboard.contrib.trove import api as trove_api
+from openstack_dashboard.contrib.trove.content.databases import db_capability
+
+LOG = logging.getLogger(__name__)
+
+
+class LaunchForm(forms.SelfHandlingForm):
+ name = forms.CharField(label=_("Cluster Name"),
+ max_length=80)
+ datastore = forms.ChoiceField(
+ label=_("Datastore"),
+ help_text=_("Type and version of datastore."),
+ widget=forms.Select(attrs={
+ 'class': 'switchable',
+ 'data-slug': 'datastore'
+ }))
+ mongodb_flavor = forms.ChoiceField(
+ label=_("Flavor"),
+ help_text=_("Size of instance to launch."),
+ required=False,
+ widget=forms.Select(attrs={
+ 'class': 'switched',
+ 'data-switch-on': 'datastore',
+ }))
+ vertica_flavor = forms.ChoiceField(
+ label=_("Flavor"),
+ help_text=_("Size of instance to launch."),
+ required=False,
+ widget=forms.Select(attrs={
+ 'class': 'switched',
+ 'data-switch-on': 'datastore',
+ }))
+ network = forms.ChoiceField(
+ label=_("Network"),
+ help_text=_("Network attached to instance."),
+ required=False)
+ volume = forms.IntegerField(
+ label=_("Volume Size"),
+ min_value=0,
+ initial=1,
+ help_text=_("Size of the volume in GB."))
+ root_password = forms.CharField(
+ label=_("Root Password"),
+ required=False,
+ help_text=_("Password for root user."),
+ widget=forms.PasswordInput(attrs={
+ 'class': 'switched',
+ 'data-switch-on': 'datastore',
+ }))
+ num_instances_vertica = forms.IntegerField(
+ label=_("Number of Instances"),
+ min_value=3,
+ initial=3,
+ required=False,
+ help_text=_("Number of instances in the cluster. (Read only)"),
+ widget=forms.TextInput(attrs={
+ 'readonly': 'readonly',
+ 'class': 'switched',
+ 'data-switch-on': 'datastore',
+ }))
+ num_shards = forms.IntegerField(
+ label=_("Number of Shards"),
+ min_value=1,
+ initial=1,
+ required=False,
+ help_text=_("Number of shards. (Read only)"),
+ widget=forms.TextInput(attrs={
+ 'readonly': 'readonly',
+ 'class': 'switched',
+ 'data-switch-on': 'datastore',
+ }))
+ num_instances_per_shards = forms.IntegerField(
+ label=_("Instances Per Shard"),
+ initial=3,
+ required=False,
+ help_text=_("Number of instances per shard. (Read only)"),
+ widget=forms.TextInput(attrs={
+ 'readonly': 'readonly',
+ 'class': 'switched',
+ 'data-switch-on': 'datastore',
+ }))
+
+ # (name of field variable, label)
+ mongodb_fields = [
+ ('mongodb_flavor', _('Flavor')),
+ ('num_shards', _('Number of Shards')),
+ ('num_instances_per_shards', _('Instances Per Shard'))
+ ]
+ vertica_fields = [
+ ('num_instances_vertica', ('Number of Instances')),
+ ('vertica_flavor', _('Flavor')),
+ ('root_password', _('Root Password')),
+ ]
+
+ def __init__(self, request, *args, **kwargs):
+ super(LaunchForm, self).__init__(request, *args, **kwargs)
+
+ self.fields['datastore'].choices = self.populate_datastore_choices(
+ request)
+ self.populate_flavor_choices(request)
+
+ self.fields['network'].choices = self.populate_network_choices(
+ request)
+
+ def clean(self):
+ datastore_field_value = self.data.get("datastore", None)
+ if datastore_field_value:
+ datastore = datastore_field_value.split(',')[0]
+
+ if db_capability.is_mongodb_datastore(datastore):
+ if self.data.get("num_shards", None) < 1:
+ msg = _("The number of shards must be greater than 1.")
+ self._errors["num_shards"] = self.error_class([msg])
+
+ elif db_capability.is_vertica_datastore(datastore):
+ if not self.data.get("vertica_flavor", None):
+ msg = _("The flavor must be specified.")
+ self._errors["vertica_flavor"] = self.error_class([msg])
+ if not self.data.get("root_password", None):
+ msg = _("Password for root user must be specified.")
+ self._errors["root_password"] = self.error_class([msg])
+
+ return self.cleaned_data
+
+ @memoized.memoized_method
+ def datastore_flavors(self, request, datastore_name, datastore_version):
+ try:
+ return trove_api.trove.datastore_flavors(
+ request, datastore_name, datastore_version)
+ except Exception:
+ LOG.exception("Exception while obtaining flavors list")
+ self._flavors = []
+ redirect = reverse('horizon:project:database_clusters:index')
+ exceptions.handle(request,
+ _('Unable to obtain flavors.'),
+ redirect=redirect)
+
+ def populate_flavor_choices(self, request):
+ valid_flavor = []
+ for ds in self.datastores(request):
+ # TODO(michayu): until capabilities lands
+ if db_capability.is_mongodb_datastore(ds.name):
+ versions = self.datastore_versions(request, ds.name)
+ for version in versions:
+ if version.name == "inactive":
+ continue
+ valid_flavor = self.datastore_flavors(request, ds.name,
+ versions[0].name)
+ if valid_flavor:
+ self.fields['mongodb_flavor'].choices = sorted(
+ [(f.id, "%s" % f.name) for f in valid_flavor])
+
+ if db_capability.is_vertica_datastore(ds.name):
+ versions = self.datastore_versions(request, ds.name)
+ for version in versions:
+ if version.name == "inactive":
+ continue
+ valid_flavor = self.datastore_flavors(request, ds.name,
+ versions[0].name)
+ if valid_flavor:
+ self.fields['vertica_flavor'].choices = sorted(
+ [(f.id, "%s" % f.name) for f in valid_flavor])
+
+ @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:
+ return trove_api.trove.datastore_list(request)
+ except Exception:
+ LOG.exception("Exception while obtaining datastores list")
+ self._datastores = []
+ redirect = reverse('horizon:project:database_clusters:index')
+ exceptions.handle(request,
+ _('Unable to obtain datastores.'),
+ redirect=redirect)
+
+ def filter_cluster_datastores(self, request):
+ datastores = []
+ for ds in self.datastores(request):
+ # TODO(michayu): until capabilities lands
+ if (db_capability.is_vertica_datastore(ds.name)
+ or db_capability.is_mongodb_datastore(ds.name)):
+ datastores.append(ds)
+ return datastores
+
+ @memoized.memoized_method
+ def datastore_versions(self, request, datastore):
+ try:
+ return trove_api.trove.datastore_version_list(request, datastore)
+ except Exception:
+ LOG.exception("Exception while obtaining datastore version list")
+ self._datastore_versions = []
+ redirect = reverse('horizon:project:database_clusters:index')
+ exceptions.handle(request,
+ _('Unable to obtain datastore versions.'),
+ redirect=redirect)
+
+ def populate_datastore_choices(self, request):
+ choices = ()
+ datastores = self.filter_cluster_datastores(request)
+ if datastores is not None:
+ for ds in datastores:
+ versions = self.datastore_versions(request, ds.name)
+ if versions:
+ # only add to choices if datastore has at least one version
+ version_choices = ()
+ for v in versions:
+ if "inactive" in v.name:
+ continue
+ selection_text = ds.name + ' - ' + v.name
+ widget_text = ds.name + '-' + v.name
+ version_choices = (version_choices +
+ ((widget_text, selection_text),))
+ self._add_attr_to_optional_fields(ds.name,
+ widget_text)
+
+ choices = choices + version_choices
+ return choices
+
+ def _add_attr_to_optional_fields(self, datastore, selection_text):
+ fields = []
+ if db_capability.is_mongodb_datastore(datastore):
+ fields = self.mongodb_fields
+ elif db_capability.is_vertica_datastore(datastore):
+ fields = self.vertica_fields
+
+ for field in fields:
+ attr_key = 'data-datastore-' + selection_text
+ widget = self.fields[field[0]].widget
+ if attr_key not in widget.attrs:
+ widget.attrs[attr_key] = field[1]
+
+ @sensitive_variables('data')
+ def handle(self, request, data):
+ try:
+ datastore = data['datastore'].split('-')[0]
+ datastore_version = data['datastore'].split('-')[1]
+
+ final_flavor = data['mongodb_flavor']
+ num_instances = data['num_instances_per_shards']
+ root_password = None
+ if db_capability.is_vertica_datastore(datastore):
+ final_flavor = data['vertica_flavor']
+ root_password = data['root_password']
+ num_instances = data['num_instances_vertica']
+ LOG.info("Launching cluster with parameters "
+ "{name=%s, volume=%s, flavor=%s, "
+ "datastore=%s, datastore_version=%s",
+ data['name'], data['volume'], final_flavor,
+ datastore, datastore_version)
+
+ trove_api.trove.cluster_create(request,
+ data['name'],
+ data['volume'],
+ final_flavor,
+ num_instances,
+ datastore=datastore,
+ datastore_version=datastore_version,
+ nics=data['network'],
+ root_password=root_password)
+ messages.success(request,
+ _('Launched cluster "%s"') % data['name'])
+ return True
+ except Exception as e:
+ redirect = reverse("horizon:project:database_clusters:index")
+ exceptions.handle(request,
+ _('Unable to launch cluster. %s') % e.message,
+ redirect=redirect)
+
+
+class AddShardForm(forms.SelfHandlingForm):
+ name = forms.CharField(
+ label=_("Cluster Name"),
+ max_length=80,
+ widget=forms.TextInput(attrs={'readonly': 'readonly'}))
+ num_shards = forms.IntegerField(
+ label=_("Number of Shards"),
+ initial=1,
+ widget=forms.TextInput(attrs={'readonly': 'readonly'}))
+ num_instances = forms.IntegerField(label=_("Instances Per Shard"),
+ initial=3,
+ widget=forms.TextInput(
+ attrs={'readonly': 'readonly'}))
+ cluster_id = forms.CharField(required=False,
+ widget=forms.HiddenInput())
+
+ def handle(self, request, data):
+ try:
+ LOG.info("Adding shard with parameters "
+ "{name=%s, num_shards=%s, num_instances=%s, "
+ "cluster_id=%s}",
+ data['name'],
+ data['num_shards'],
+ data['num_instances'],
+ data['cluster_id'])
+ trove_api.trove.cluster_add_shard(request, data['cluster_id'])
+
+ messages.success(request,
+ _('Added shard to "%s"') % data['name'])
+ except Exception as e:
+ redirect = reverse("horizon:project:database_clusters:index")
+ exceptions.handle(request,
+ _('Unable to add shard. %s') % e.message,
+ redirect=redirect)
+ return True
+
+
+class ResetPasswordForm(forms.SelfHandlingForm):
+ cluster_id = forms.CharField(widget=forms.HiddenInput())
+ password = forms.CharField(widget=forms.PasswordInput(),
+ label=_("New Password"),
+ required=True,
+ help_text=_("New password for cluster access."))
+
+ @sensitive_variables('data')
+ def handle(self, request, data):
+ password = data.get("password")
+ cluster_id = data.get("cluster_id")
+ try:
+ trove_api.trove.create_cluster_root(request,
+ cluster_id,
+ password)
+ messages.success(request, _('Root password updated for '
+ 'cluster "%s"') % cluster_id)
+ except Exception as e:
+ redirect = reverse("horizon:project:database_clusters:index")
+ exceptions.handle(request, _('Unable to reset password. %s') %
+ e.message, redirect=redirect)
+ return True
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/panel.py b/openstack_dashboard/contrib/trove/content/database_clusters/panel.py
new file mode 100644
index 0000000000..1575ff376c
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/panel.py
@@ -0,0 +1,26 @@
+# 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.
+
+from django.utils.translation import ugettext_lazy as _
+
+import horizon
+
+
+class Clusters(horizon.Panel):
+ name = _("Clusters")
+ slug = 'database_clusters'
+ permissions = ('openstack.services.database',
+ 'openstack.services.object-store',)
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/tables.py b/openstack_dashboard/contrib/trove/content/database_clusters/tables.py
new file mode 100644
index 0000000000..4e9f162f73
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/tables.py
@@ -0,0 +1,205 @@
+# 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.
+
+from django.core import urlresolvers
+from django.template.defaultfilters import title # noqa
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ungettext_lazy
+
+from horizon import tables
+from horizon.templatetags import sizeformat
+from horizon.utils import filters
+from horizon.utils import memoized
+from openstack_dashboard.contrib.trove import api
+from openstack_dashboard.contrib.trove.content.databases import db_capability
+
+ACTIVE_STATES = ("ACTIVE",)
+
+
+class TerminateCluster(tables.BatchAction):
+ name = "terminate"
+ icon = "remove"
+ classes = ('btn-danger',)
+ help_text = _("Terminated cluster is not recoverable.")
+
+ @staticmethod
+ def action_present(count):
+ return ungettext_lazy(
+ u"Terminate Cluster",
+ u"Terminate Clusters",
+ count
+ )
+
+ @staticmethod
+ def action_past(count):
+ return ungettext_lazy(
+ u"Scheduled termination of Cluster",
+ u"Scheduled termination of Clusters",
+ count
+ )
+
+ def action(self, request, obj_id):
+ api.trove.cluster_delete(request, obj_id)
+
+
+class LaunchLink(tables.LinkAction):
+ name = "launch"
+ verbose_name = _("Launch Cluster")
+ url = "horizon:project:database_clusters:launch"
+ classes = ("btn-launch", "ajax-modal")
+ icon = "cloud-upload"
+
+
+class AddShard(tables.LinkAction):
+ name = "add_shard"
+ verbose_name = _("Add Shard")
+ url = "horizon:project:database_clusters:add_shard"
+ classes = ("ajax-modal",)
+ icon = "plus"
+
+ def allowed(self, request, cluster=None):
+ if (cluster and cluster.task["name"] == 'NONE' and
+ db_capability.is_mongodb_datastore(cluster.datastore['type'])):
+ return True
+ return False
+
+
+class ResetPassword(tables.LinkAction):
+ name = "reset_password"
+ verbose_name = _("Reset Root Password")
+ url = "horizon:project:database_clusters:reset_password"
+ classes = ("ajax-modal",)
+
+ def allowed(self, request, cluster=None):
+ if (cluster and cluster.task["name"] == 'NONE' and
+ db_capability.is_vertica_datastore(cluster.datastore['type'])):
+ return True
+ return False
+
+ def get_link_url(self, datum):
+ cluster_id = self.table.get_object_id(datum)
+ return urlresolvers.reverse(self.url, args=[cluster_id])
+
+
+class UpdateRow(tables.Row):
+ ajax = True
+
+ @memoized.memoized_method
+ def get_data(self, request, cluster_id):
+ cluster = api.trove.cluster_get(request, cluster_id)
+ try:
+ # TODO(michayu): assumption that cluster is homogeneous
+ flavor_id = cluster.instances[0]['flavor']['id']
+ cluster.full_flavor = api.trove.flavor_get(request, flavor_id)
+ except Exception:
+ pass
+ return cluster
+
+
+def get_datastore(cluster):
+ return cluster.datastore["type"]
+
+
+def get_datastore_version(cluster):
+ return cluster.datastore["version"]
+
+
+def get_size(cluster):
+ if db_capability.is_vertica_datastore(cluster.datastore['type']):
+ return "3"
+
+ if hasattr(cluster, "full_flavor"):
+ size_string = _("%(name)s | %(RAM)s RAM | %(instances)s instances")
+ vals = {'name': cluster.full_flavor.name,
+ 'RAM': sizeformat.mbformat(cluster.full_flavor.ram),
+ 'instances': len(cluster.instances)}
+ return size_string % vals
+ return _("Not available")
+
+
+def get_task(cluster):
+ return cluster.task["name"]
+
+
+class ClustersTable(tables.DataTable):
+ TASK_CHOICES = (
+ ("none", True),
+ )
+ name = tables.Column("name",
+ link=("horizon:project:database_clusters:detail"),
+ verbose_name=_("Cluster Name"))
+ datastore = tables.Column(get_datastore,
+ verbose_name=_("Datastore"))
+ datastore_version = tables.Column(get_datastore_version,
+ verbose_name=_("Datastore Version"))
+ size = tables.Column(get_size,
+ verbose_name=_("Cluster Size"),
+ attrs={'data-type': 'size'})
+ task = tables.Column(get_task,
+ filters=(title, filters.replace_underscores),
+ verbose_name=_("Current Task"),
+ status=True,
+ status_choices=TASK_CHOICES)
+
+ class Meta(object):
+ name = "clusters"
+ verbose_name = _("Clusters")
+ status_columns = ["task"]
+ row_class = UpdateRow
+ table_actions = (LaunchLink, TerminateCluster)
+ row_actions = (AddShard, ResetPassword, TerminateCluster)
+
+
+def get_instance_size(instance):
+ if hasattr(instance, "full_flavor"):
+ size_string = _("%(name)s | %(RAM)s RAM")
+ vals = {'name': instance.full_flavor.name,
+ 'RAM': sizeformat.mbformat(instance.full_flavor.ram)}
+ return size_string % vals
+ return _("Not available")
+
+
+def get_instance_type(instance):
+ if hasattr(instance, "type"):
+ return instance.type
+ return _("Not available")
+
+
+def get_host(instance):
+ if hasattr(instance, "hostname"):
+ return instance.hostname
+ elif hasattr(instance, "ip") and instance.ip:
+ return instance.ip[0]
+ return _("Not Assigned")
+
+
+class InstancesTable(tables.DataTable):
+ name = tables.Column("name",
+ verbose_name=_("Name"))
+ type = tables.Column(get_instance_type,
+ verbose_name=_("Type"))
+ host = tables.Column(get_host,
+ verbose_name=_("Host"))
+ size = tables.Column(get_instance_size,
+ verbose_name=_("Size"),
+ attrs={'data-type': 'size'})
+ status = tables.Column("status",
+ filters=(title, filters.replace_underscores),
+ verbose_name=_("Status"))
+
+ class Meta(object):
+ name = "instances"
+ verbose_name = _("Instances")
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/tabs.py b/openstack_dashboard/contrib/trove/content/database_clusters/tabs.py
new file mode 100644
index 0000000000..e107bd5600
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/tabs.py
@@ -0,0 +1,84 @@
+# 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.
+
+from django import template
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import tabs
+from openstack_dashboard.contrib.trove import api
+from openstack_dashboard.contrib.trove.content.database_clusters import tables
+
+
+class OverviewTab(tabs.Tab):
+ name = _("Overview")
+ slug = "overview"
+
+ def get_context_data(self, request):
+ return {"cluster": self.tab_group.kwargs['cluster']}
+
+ def get_template_name(self, request):
+ cluster = self.tab_group.kwargs['cluster']
+ template_file = ('project/database_clusters/_detail_overview_%s.html'
+ % cluster.datastore['type'])
+ try:
+ template.loader.get_template(template_file)
+ return template_file
+ except template.TemplateDoesNotExist:
+ # This datastore type does not have a template file
+ # Just use the base template file
+ return ('project/database_clusters/_detail_overview.html')
+
+
+class InstancesTab(tabs.TableTab):
+ table_classes = (tables.InstancesTable,)
+ name = _("Instances")
+ slug = "instances_tab"
+ cluster = None
+ template_name = "horizon/common/_detail_table.html"
+ preload = True
+
+ def get_instances_data(self):
+ cluster = self.tab_group.kwargs['cluster']
+ data = []
+ try:
+ instances = api.trove.cluster_get(self.request,
+ cluster.id).instances
+ for instance in instances:
+ instance_info = api.trove.instance_get(self.request,
+ instance['id'])
+ flavor_id = instance_info.flavor['id']
+ instance_info.full_flavor = api.trove.flavor_get(self.request,
+ flavor_id)
+ if "type" in instance:
+ instance_info.type = instance["type"]
+ if "ip" in instance:
+ instance_info.ip = instance["ip"]
+ if "hostname" in instance:
+ instance_info.hostname = instance["hostname"]
+
+ data.append(instance_info)
+ except Exception:
+ msg = _('Unable to get instances data.')
+ exceptions.handle(self.request, msg)
+ data = []
+ return data
+
+
+class ClusterDetailTabs(tabs.TabGroup):
+ slug = "cluster_details"
+ tabs = (OverviewTab, InstancesTab)
+ sticky = True
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_add_shard.html b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_add_shard.html
new file mode 100644
index 0000000000..e26a782210
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_add_shard.html
@@ -0,0 +1,25 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}add_shard_form{% endblock %}
+{% block form_action %}{% url "horizon:project:database_clusters:add_shard" cluster_id %}{% endblock %}
+
+{% block modal_id %}add_shard_modal{% endblock %}
+{% block modal-header %}{% trans "Add Shard" %}{% endblock %}
+
+{% block modal-body %}
+
+
+
+
+
{% blocktrans %}Specify the details for adding additional shards.{% endblocktrans %}
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% endblock %}
\ No newline at end of file
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_detail_overview.html b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_detail_overview.html
new file mode 100644
index 0000000000..451b1dde79
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_detail_overview.html
@@ -0,0 +1,27 @@
+{% load i18n sizeformat %}
+
+{% trans "Cluster Overview" %}
+
+
+
{% trans "Information" %}
+
+
+ - {% trans "Name" %}
+ - {{ cluster.name }}
+ - {% trans "ID" %}
+ - {{ cluster.id }}
+ - {% trans "Datastore" %}
+ - {{ cluster.datastore.type }}
+ - {% trans "Datastore Version" %}
+ - {{ cluster.datastore.version }}
+ - {% trans "Current Task" %}
+ - {{ cluster.task.name|title }}
+ - {% trans "RAM" %}
+ - {{ cluster.full_flavor.ram|mbformat }}
+ - {% trans "Number of Instances" %}
+ - {{ cluster.num_instances }}
+
+
+
+{% block connection_info %}
+{% endblock %}
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_detail_overview_mongodb.html b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_detail_overview_mongodb.html
new file mode 100644
index 0000000000..d215a36e6c
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_detail_overview_mongodb.html
@@ -0,0 +1,27 @@
+{% extends "project/database_clusters/_detail_overview.html" %}
+{% load i18n sizeformat %}
+
+{% block connection_info %}
+
+
{% trans "Connection Information" %}
+
+
+ {% with cluster.ip.0 as ip %}
+ - {% trans "Host" %}
+ -
+ {% if not ip %}
+ {% trans "Not Assigned" %}
+
+ {% else %}
+ {{ ip }}
+
+ - {% trans "Database Port" %}
+ - 27017
+ - {% trans "Connection Examples" %}
+ - mongo --host {{ ip }}
+ - mongodb://[{% trans "USERNAME" %}:{% trans "PASSWORD" %}@]{{ ip }}:27017/{% trans "DATABASE" %}
+ {% endif %}
+ {% endwith %}
+
+
+{% endblock %}
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_detail_overview_vertica.html b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_detail_overview_vertica.html
new file mode 100644
index 0000000000..22d823acf6
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_detail_overview_vertica.html
@@ -0,0 +1,29 @@
+{% load i18n sizeformat %}
+
+{% trans "Cluster Overview" %}
+
+
+
{% trans "Information" %}
+
+
+ - {% trans "Name" %}
+ - {{ cluster.name }}
+ - {% trans "ID" %}
+ - {{ cluster.id }}
+ - {% trans "Datastore" %}
+ - {{ cluster.datastore.type }}
+ - {% trans "Datastore Version" %}
+ - {{ cluster.datastore.version }}
+ - {% trans "Current Task" %}
+ - {{ cluster.task.name|title }}
+ - {% trans "RAM" %}
+ - {{ cluster.full_flavor.ram|mbformat }}
+ - {% trans "Number of Instances" %}
+ - {{ cluster.num_instances }}
+ - {% trans "Managment Console" %}
+ - {{ cluster.mgmt_url }}
+
+
+
+{% block connection_info %}
+{% endblock %}
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_launch.html b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_launch.html
new file mode 100644
index 0000000000..3753b75b6a
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_launch.html
@@ -0,0 +1,22 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}launch_form{% endblock %}
+{% block form_action %}{% url "horizon:project:database_clusters:launch" %}{% endblock %}
+
+{% block modal_id %}launch_modal{% endblock %}
+{% block modal-header %}{% trans "Launch Cluster" %}{% endblock %}
+
+{% block modal-body %}
+
+
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% endblock %}
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_reset_password.html b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_reset_password.html
new file mode 100644
index 0000000000..9b27084784
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/_reset_password.html
@@ -0,0 +1,25 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}reset_password_form{% endblock %}
+{% block form_action %}{% url "horizon:project:database_clusters:reset_password" cluster_id %}{% endblock %}
+
+{% block modal_id %}reset_password_modal{% endblock %}
+{% block modal-header %}{% trans "Reset Root Password" %}{% endblock %}
+
+{% block modal-body %}
+
+
+
+
+
{% blocktrans %}Specify the new root password for vertica cluster.{% endblocktrans %}
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% endblock %}
\ No newline at end of file
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/add_shard.html b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/add_shard.html
new file mode 100644
index 0000000000..ef9d23d332
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/add_shard.html
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% block title %}{% trans "Add Shard" %}{% endblock %}
+
+{% block main %}
+ {% include "project/database_clusters/_add_shard.html" %}
+{% endblock %}
\ No newline at end of file
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/detail.html b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/detail.html
new file mode 100644
index 0000000000..6906b819a0
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/detail.html
@@ -0,0 +1,12 @@
+{% extends 'base.html' %}
+{% load i18n sizeformat %}
+{% block title %}{% trans "Cluster Detail" %}{% endblock %}
+
+{% block main %}
+
+
+ {{ tab_group.render }}
+
+
+{% endblock %}
+
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/index.html b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/index.html
new file mode 100644
index 0000000000..c0212f01bb
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/index.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Clusters" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Clusters") %}
+{% endblock page_header %}
+
+{% block main %}
+ {{ table.render }}
+{% endblock %}
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/launch.html b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/launch.html
new file mode 100644
index 0000000000..5fb645bf8e
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/launch.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Launch Cluster" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Launch Cluster") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'project/database_clusters/_launch.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/reset_password.html b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/reset_password.html
new file mode 100644
index 0000000000..cb90724b97
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/templates/database_clusters/reset_password.html
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% block title %}{% trans "Reset Root Password" %}{% endblock %}
+
+{% block main %}
+ {% include "project/database_clusters/_reset_password.html" %}
+{% endblock %}
\ No newline at end of file
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/tests.py b/openstack_dashboard/contrib/trove/content/database_clusters/tests.py
new file mode 100644
index 0000000000..61ec0b6024
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/tests.py
@@ -0,0 +1,295 @@
+# 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.
+
+from django.core.urlresolvers import reverse
+from django import http
+
+from mox3.mox import IsA # noqa
+
+from openstack_dashboard import api
+from openstack_dashboard.contrib.trove import api as trove_api
+from openstack_dashboard.test import helpers as test
+
+from troveclient import common
+
+
+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'])
+ADD_SHARD_VIEWNAME = 'horizon:project:database_clusters:add_shard'
+RESET_PASSWORD_VIEWNAME = 'horizon:project:database_clusters:reset_password'
+
+
+class ClustersTests(test.TestCase):
+ @test.create_stubs({trove_api.trove: ('cluster_list',
+ 'flavor_list')})
+ def test_index(self):
+ clusters = common.Paginated(self.trove_clusters.list())
+ trove_api.trove.cluster_list(IsA(http.HttpRequest), marker=None)\
+ .AndReturn(clusters)
+ trove_api.trove.flavor_list(IsA(http.HttpRequest))\
+ .AndReturn(self.flavors.list())
+
+ self.mox.ReplayAll()
+ res = self.client.get(INDEX_URL)
+ self.assertTemplateUsed(res, 'project/database_clusters/index.html')
+
+ @test.create_stubs({trove_api.trove: ('cluster_list',
+ 'flavor_list')})
+ def test_index_flavor_exception(self):
+ clusters = common.Paginated(self.trove_clusters.list())
+ trove_api.trove.cluster_list(IsA(http.HttpRequest), marker=None)\
+ .AndReturn(clusters)
+ trove_api.trove.flavor_list(IsA(http.HttpRequest))\
+ .AndRaise(self.exceptions.trove)
+
+ self.mox.ReplayAll()
+ res = self.client.get(INDEX_URL)
+ self.assertTemplateUsed(res, 'project/database_clusters/index.html')
+ self.assertMessageCount(res, error=1)
+
+ @test.create_stubs({trove_api.trove: ('cluster_list',)})
+ def test_index_list_exception(self):
+ trove_api.trove.cluster_list(IsA(http.HttpRequest), marker=None)\
+ .AndRaise(self.exceptions.trove)
+
+ self.mox.ReplayAll()
+ res = self.client.get(INDEX_URL)
+ self.assertTemplateUsed(res, 'project/database_clusters/index.html')
+ self.assertMessageCount(res, error=1)
+
+ @test.create_stubs({trove_api.trove: ('cluster_list',
+ 'flavor_list')})
+ def test_index_pagination(self):
+ clusters = self.trove_clusters.list()
+ last_record = clusters[0]
+ clusters = common.Paginated(clusters, next_marker="foo")
+ trove_api.trove.cluster_list(IsA(http.HttpRequest), marker=None)\
+ .AndReturn(clusters)
+ trove_api.trove.flavor_list(IsA(http.HttpRequest))\
+ .AndReturn(self.flavors.list())
+
+ self.mox.ReplayAll()
+ res = self.client.get(INDEX_URL)
+ self.assertTemplateUsed(res, 'project/database_clusters/index.html')
+ self.assertContains(
+ res, 'marker=' + last_record.id)
+
+ @test.create_stubs({trove_api.trove: ('cluster_list',
+ 'flavor_list')})
+ def test_index_flavor_list_exception(self):
+ clusters = common.Paginated(self.trove_clusters.list())
+ trove_api.trove.cluster_list(IsA(http.HttpRequest), marker=None)\
+ .AndReturn(clusters)
+ trove_api.trove.flavor_list(IsA(http.HttpRequest))\
+ .AndRaise(self.exceptions.trove)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(INDEX_URL)
+
+ self.assertTemplateUsed(res, 'project/database_clusters/index.html')
+ self.assertMessageCount(res, error=1)
+
+ @test.create_stubs({trove_api.trove: ('datastore_flavors',
+ 'datastore_list',
+ 'datastore_version_list'),
+ api.base: ['is_service_enabled']})
+ def test_launch_cluster(self):
+ api.base.is_service_enabled(IsA(http.HttpRequest), 'network')\
+ .AndReturn(False)
+ trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
+ 'mongodb', '2.6')\
+ .AndReturn(self.flavors.list())
+ trove_api.trove.datastore_list(IsA(http.HttpRequest))\
+ .AndReturn(self.datastores.list())
+ trove_api.trove.datastore_version_list(IsA(http.HttpRequest),
+ IsA(str))\
+ .AndReturn(self.datastore_versions.list())
+ self.mox.ReplayAll()
+ res = self.client.get(LAUNCH_URL)
+ self.assertTemplateUsed(res, 'project/database_clusters/launch.html')
+
+ @test.create_stubs({trove_api.trove: ['datastore_flavors',
+ 'cluster_create',
+ 'datastore_list',
+ 'datastore_version_list'],
+ api.base: ['is_service_enabled']})
+ def test_create_simple_cluster(self):
+ api.base.is_service_enabled(IsA(http.HttpRequest), 'network')\
+ .AndReturn(False)
+ trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
+ 'mongodb', '2.6')\
+ .AndReturn(self.flavors.list())
+ trove_api.trove.datastore_list(IsA(http.HttpRequest))\
+ .AndReturn(self.datastores.list())
+ trove_api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
+ .AndReturn(self.datastore_versions.list())
+
+ 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''
+ trove_api.trove.cluster_create(
+ IsA(http.HttpRequest),
+ cluster_name,
+ cluster_volume,
+ cluster_flavor,
+ cluster_instances,
+ datastore=cluster_datastore,
+ datastore_version=cluster_datastore_version,
+ nics=cluster_network,
+ root_password=None).AndReturn(self.trove_clusters.first())
+
+ self.mox.ReplayAll()
+ post = {
+ 'name': cluster_name,
+ 'volume': cluster_volume,
+ 'num_instances': cluster_instances,
+ 'num_shards': 1,
+ 'num_instances_per_shards': cluster_instances,
+ 'datastore': cluster_datastore + u'-' + cluster_datastore_version,
+ 'mongodb_flavor': cluster_flavor,
+ 'network': cluster_network
+ }
+
+ res = self.client.post(LAUNCH_URL, post)
+ self.assertNoFormErrors(res)
+ self.assertMessageCount(success=1)
+
+ @test.create_stubs({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):
+ api.base.is_service_enabled(IsA(http.HttpRequest), 'network')\
+ .AndReturn(True)
+ api.neutron.network_list_for_tenant(IsA(http.HttpRequest), '1')\
+ .AndReturn(self.networks.list())
+ trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
+ 'mongodb', '2.6')\
+ .AndReturn(self.flavors.list())
+ trove_api.trove.datastore_list(IsA(http.HttpRequest))\
+ .AndReturn(self.datastores.list())
+ trove_api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
+ .AndReturn(self.datastore_versions.list())
+
+ 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'
+ trove_api.trove.cluster_create(
+ IsA(http.HttpRequest),
+ cluster_name,
+ cluster_volume,
+ cluster_flavor,
+ cluster_instances,
+ datastore=cluster_datastore,
+ datastore_version=cluster_datastore_version,
+ nics=cluster_network,
+ root_password=None).AndReturn(self.trove_clusters.first())
+
+ self.mox.ReplayAll()
+ post = {
+ 'name': cluster_name,
+ 'volume': cluster_volume,
+ 'num_instances': cluster_instances,
+ 'num_shards': 1,
+ 'num_instances_per_shards': cluster_instances,
+ 'datastore': cluster_datastore + u'-' + cluster_datastore_version,
+ 'mongodb_flavor': cluster_flavor,
+ 'network': cluster_network
+ }
+
+ res = self.client.post(LAUNCH_URL, post)
+ self.assertNoFormErrors(res)
+ self.assertMessageCount(success=1)
+
+ @test.create_stubs({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):
+ api.neutron.network_list_for_tenant(IsA(http.HttpRequest), '1')\
+ .AndReturn(self.networks.list())
+ trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
+ 'mongodb', '2.6')\
+ .AndReturn(self.flavors.list())
+ trove_api.trove.datastore_list(IsA(http.HttpRequest))\
+ .AndReturn(self.datastores.list())
+ trove_api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
+ .AndReturn(self.datastore_versions.list())
+
+ 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'
+ trove_api.trove.cluster_create(
+ IsA(http.HttpRequest),
+ cluster_name,
+ cluster_volume,
+ cluster_flavor,
+ cluster_instances,
+ datastore=cluster_datastore,
+ datastore_version=cluster_datastore_version,
+ nics=cluster_network,
+ root_password=None).AndReturn(self.trove_clusters.first())
+
+ self.mox.ReplayAll()
+ post = {
+ 'name': cluster_name,
+ 'volume': cluster_volume,
+ 'num_instances': cluster_instances,
+ 'num_shards': 1,
+ 'num_instances_per_shards': cluster_instances,
+ 'datastore': cluster_datastore + u'-' + cluster_datastore_version,
+ 'mongodb_flavor': cluster_flavor,
+ 'network': cluster_network
+ }
+
+ res = self.client.post(LAUNCH_URL, post)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({trove_api.trove: ('cluster_get',
+ 'instance_get',
+ 'flavor_get',)})
+ def test_details(self):
+ cluster = self.trove_clusters.first()
+ trove_api.trove.cluster_get(IsA(http.HttpRequest), cluster.id)\
+ .MultipleTimes().AndReturn(cluster)
+ trove_api.trove.instance_get(IsA(http.HttpRequest), IsA(str))\
+ .MultipleTimes().AndReturn(self.databases.first())
+ trove_api.trove.flavor_get(IsA(http.HttpRequest), IsA(str))\
+ .MultipleTimes().AndReturn(self.flavors.first())
+ self.mox.ReplayAll()
+
+ details_url = reverse('horizon:project:database_clusters:detail',
+ args=[cluster.id])
+ res = self.client.get(details_url)
+ self.assertTemplateUsed(res, 'project/database_clusters/detail.html')
+ self.assertContains(res, cluster.ip[0])
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/urls.py b/openstack_dashboard/contrib/trove/content/database_clusters/urls.py
new file mode 100644
index 0000000000..1962457680
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/urls.py
@@ -0,0 +1,34 @@
+# 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.
+
+from django.conf.urls import patterns # noqa
+from django.conf.urls import url # noqa
+
+from openstack_dashboard.contrib.trove.content.database_clusters import views
+
+CLUSTERS = r'^(?P[^/]+)/%s$'
+
+urlpatterns = patterns(
+ '',
+ url(r'^$', views.IndexView.as_view(), name='index'),
+ url(r'^launch$', views.LaunchClusterView.as_view(), name='launch'),
+ url(r'^(?P[^/]+)/$', views.DetailView.as_view(),
+ name='detail'),
+ url(CLUSTERS % 'add_shard', views.AddShardView.as_view(),
+ name='add_shard'),
+ url(CLUSTERS % 'reset_password', views.ResetPasswordView.as_view(),
+ name='reset_password'),
+)
diff --git a/openstack_dashboard/contrib/trove/content/database_clusters/views.py b/openstack_dashboard/contrib/trove/content/database_clusters/views.py
new file mode 100644
index 0000000000..b66c2791da
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/database_clusters/views.py
@@ -0,0 +1,212 @@
+# 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.
+
+"""
+Views for managing database clusters.
+"""
+from collections import OrderedDict
+import logging
+
+from django.core.urlresolvers import reverse
+from django.core.urlresolvers import reverse_lazy
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import forms as horizon_forms
+from horizon import tables as horizon_tables
+from horizon import tabs as horizon_tabs
+from horizon.utils import memoized
+
+from openstack_dashboard.contrib.trove import api
+from openstack_dashboard.contrib.trove.content.database_clusters import forms
+from openstack_dashboard.contrib.trove.content.database_clusters import tables
+from openstack_dashboard.contrib.trove.content.database_clusters import tabs
+
+
+LOG = logging.getLogger(__name__)
+
+
+class IndexView(horizon_tables.DataTableView):
+ table_class = tables.ClustersTable
+ template_name = 'project/database_clusters/index.html'
+
+ def has_more_data(self, table):
+ return self._more
+
+ @memoized.memoized_method
+ def get_flavors(self):
+ try:
+ flavors = api.trove.flavor_list(self.request)
+ except Exception:
+ flavors = []
+ msg = _('Unable to retrieve database size information.')
+ exceptions.handle(self.request, msg)
+ return OrderedDict((unicode(flavor.id), flavor) for flavor in flavors)
+
+ def _extra_data(self, cluster):
+ try:
+ cluster_flavor = cluster.instances[0]["flavor"]["id"]
+ flavors = self.get_flavors()
+ flavor = flavors.get(cluster_flavor)
+ if flavor is not None:
+ cluster.full_flavor = flavor
+ except Exception:
+ # ignore any errors and just return cluster unaltered
+ pass
+ return cluster
+
+ def get_data(self):
+ marker = self.request.GET.get(
+ tables.ClustersTable._meta.pagination_param)
+ # Gather our clusters
+ try:
+ clusters = api.trove.cluster_list(self.request, marker=marker)
+ self._more = clusters.next or False
+ except Exception:
+ self._more = False
+ clusters = []
+ msg = _('Unable to retrieve database clusters.')
+ exceptions.handle(self.request, msg)
+
+ map(self._extra_data, clusters)
+
+ return clusters
+
+
+class LaunchClusterView(horizon_forms.ModalFormView):
+ form_class = forms.LaunchForm
+ template_name = 'project/database_clusters/launch.html'
+ success_url = reverse_lazy('horizon:project:database_clusters:index')
+
+
+class DetailView(horizon_tabs.TabbedTableView):
+ tab_group_class = tabs.ClusterDetailTabs
+ template_name = 'project/database_clusters/detail.html'
+
+ page_title = _("Cluster Details: {{ cluster.name }}")
+
+ def get_context_data(self, **kwargs):
+ context = super(DetailView, self).get_context_data(**kwargs)
+ context["cluster"] = self.get_data()
+ return context
+
+ @memoized.memoized_method
+ def get_data(self):
+ try:
+ cluster_id = self.kwargs['cluster_id']
+ cluster = api.trove.cluster_get(self.request, cluster_id)
+ except Exception:
+ redirect = reverse('horizon:project:database_clusters:index')
+ msg = _('Unable to retrieve details '
+ 'for database cluster: %s') % cluster_id
+ exceptions.handle(self.request, msg, redirect=redirect)
+ try:
+ cluster.full_flavor = api.trove.flavor_get(
+ self.request, cluster.instances[0]["flavor"]["id"])
+ except Exception:
+ LOG.error('Unable to retrieve flavor details'
+ ' for database cluster: %s' % cluster_id)
+ cluster.num_instances = len(cluster.instances)
+
+ # Todo(saurabhs) Set mgmt_url to dispaly Mgmt Console URL on
+ # cluster details page
+ # for instance in cluster.instances:
+ # if instance['type'] == "master":
+ # cluster.mgmt_url = "https://%s:5450/webui" % instance['ip'][0]
+
+ return cluster
+
+ def get_tabs(self, request, *args, **kwargs):
+ cluster = self.get_data()
+ return self.tab_group_class(request, cluster=cluster, **kwargs)
+
+
+class AddShardView(horizon_forms.ModalFormView):
+ form_class = forms.AddShardForm
+ template_name = 'project/database_clusters/add_shard.html'
+ success_url = reverse_lazy('horizon:project:database_clusters:index')
+ page_title = _("Add Shard")
+
+ def get_context_data(self, **kwargs):
+ context = super(AddShardView, self).get_context_data(**kwargs)
+ context["cluster_id"] = self.kwargs['cluster_id']
+ return context
+
+ def get_object(self, *args, **kwargs):
+ if not hasattr(self, "_object"):
+ cluster_id = self.kwargs['cluster_id']
+ try:
+ self._object = api.trove.cluster_get(self.request, cluster_id)
+ # TODO(michayu): assumption that cluster is homogeneous
+ flavor_id = self._object.instances[0]['flavor']['id']
+ flavors = self.get_flavors()
+ if flavor_id in flavors:
+ self._object.flavor_name = flavors[flavor_id].name
+ else:
+ flavor = api.trove.flavor_get(self.request, flavor_id)
+ self._object.flavor_name = flavor.name
+ except Exception:
+ redirect = reverse("horizon:project:database_clusters:index")
+ msg = _('Unable to retrieve cluster details.')
+ exceptions.handle(self.request, msg, redirect=redirect)
+ return self._object
+
+ def get_flavors(self, *args, **kwargs):
+ if not hasattr(self, "_flavors"):
+ try:
+ flavors = api.trove.flavor_list(self.request)
+ self._flavors = OrderedDict([(str(flavor.id), flavor)
+ for flavor in flavors])
+ except Exception:
+ redirect = reverse("horizon:project:database_clusters:index")
+ exceptions.handle(
+ self.request,
+ _('Unable to retrieve flavors.'), redirect=redirect)
+ return self._flavors
+
+ def get_initial(self):
+ initial = super(AddShardView, self).get_initial()
+ _object = self.get_object()
+ if _object:
+ initial.update(
+ {'cluster_id': self.kwargs['cluster_id'],
+ 'name': getattr(_object, 'name', None)})
+ return initial
+
+
+class ResetPasswordView(horizon_forms.ModalFormView):
+ form_class = forms.ResetPasswordForm
+ template_name = 'project/database_clusters/reset_password.html'
+ success_url = reverse_lazy('horizon:project:database_clusters:index')
+ page_title = _("Reset Root Password")
+
+ @memoized.memoized_method
+ def get_object(self, *args, **kwargs):
+ cluster_id = self.kwargs['cluster_id']
+ try:
+ return api.trove.cluster_get(self.request, cluster_id)
+ except Exception:
+ msg = _('Unable to retrieve cluster details.')
+ redirect = reverse('horizon:project:database_clusters:index')
+ exceptions.handle(self.request, msg, redirect=redirect)
+
+ def get_context_data(self, **kwargs):
+ context = super(ResetPasswordView, self).get_context_data(**kwargs)
+ context['cluster_id'] = self.kwargs['cluster_id']
+ return context
+
+ def get_initial(self):
+ return {'cluster_id': self.kwargs['cluster_id']}
diff --git a/openstack_dashboard/contrib/trove/content/databases/db_capability.py b/openstack_dashboard/contrib/trove/content/databases/db_capability.py
new file mode 100644
index 0000000000..8f63eb3879
--- /dev/null
+++ b/openstack_dashboard/contrib/trove/content/databases/db_capability.py
@@ -0,0 +1,25 @@
+# Copyright 2015 Tesora Inc.
+#
+# 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.
+
+
+MONGODB = "mongodb"
+VERTICA = "vertica"
+
+
+def is_mongodb_datastore(datastore):
+ return (datastore is not None) and (MONGODB in datastore.lower())
+
+
+def is_vertica_datastore(datastore):
+ return (datastore is not None) and (VERTICA in datastore.lower())
diff --git a/openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_cassandra.html b/openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_cassandra.html
index 26dac1adaa..0b2f4d4d88 100644
--- a/openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_cassandra.html
+++ b/openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_cassandra.html
@@ -3,7 +3,7 @@
{% block connection_info %}
-
{% trans "Connection Info" %}
+
{% trans "Connection Information" %}
{% with instance.host as host %}
@@ -20,4 +20,4 @@
{% endwith %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_mongodb.html b/openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_mongodb.html
index 6c200ef147..28f3d222d4 100644
--- a/openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_mongodb.html
+++ b/openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_mongodb.html
@@ -3,22 +3,30 @@
{% block connection_info %}
-
{% trans "Connection Info" %}
+
{% trans "Connection Information" %}
- {% with instance.host as host %}
- - {% trans "Host" %}
- {% if not host %}
- - {% trans "Not Assigned" %}
- {% else %}
- - {{ host }}
- - {% trans "Database Port" %}
- - 27017
- - {% trans "Connection Examples" %}
- - mongo --host {{ host }}
- - mongodb://[{% trans "USERNAME" %}:{% trans "PASSWORD" %}@]{{ host }}:27017/{% trans "DATABASE" %}
- {% endif %}
- {% endwith %}
-
-
-{% endblock %}
\ No newline at end of file
+ {% if instance.cluster_id %}
+ Link to Cluster Details for Connection Info
+ {% else %}
+
+ {% with instance.host as host %}
+ - {% trans "Host" %}
+ -
+ {% if not host %}
+ {% trans "Not Assigned" %}
+
+ {% else %}
+ {{ host }}
+
+ - {% trans "Database Port" %}
+ - 27017
+ - {% trans "Connection Examples" %}
+ - mongo --host {{ host }}
+ - mongodb://[{% trans "USERNAME" %}:{% trans "PASSWORD" %}@]{{ host }}:27017/{% trans "DATABASE" %}
+ {% endif %}
+ {% endwith %}
+
+ {% endif %}
+
+{% endblock %}
diff --git a/openstack_dashboard/contrib/trove/content/databases/tests.py b/openstack_dashboard/contrib/trove/content/databases/tests.py
index e242fea793..52ff21b5ae 100644
--- a/openstack_dashboard/contrib/trove/content/databases/tests.py
+++ b/openstack_dashboard/contrib/trove/content/databases/tests.py
@@ -140,7 +140,7 @@ class DatabaseTests(test.TestCase):
self.datastores.list())
# Mock datastore versions
api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str)).\
- AndReturn(self.datastore_versions.list())
+ MultipleTimes().AndReturn(self.datastore_versions.list())
dash_api.neutron.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id,
@@ -207,7 +207,7 @@ class DatabaseTests(test.TestCase):
# Mock datastore versions
api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
- .AndReturn(self.datastore_versions.list())
+ .MultipleTimes().AndReturn(self.datastore_versions.list())
dash_api.neutron.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id,
@@ -268,7 +268,7 @@ class DatabaseTests(test.TestCase):
# Mock datastore versions
api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
- .AndReturn(self.datastore_versions.list())
+ .MultipleTimes().AndReturn(self.datastore_versions.list())
dash_api.neutron.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id,
@@ -499,7 +499,7 @@ class DatabaseTests(test.TestCase):
api.trove.datastore_version_list(IsA(http.HttpRequest),
IsA(str))\
- .AndReturn(self.datastore_versions.list())
+ .MultipleTimes().AndReturn(self.datastore_versions.list())
dash_api.neutron.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id,
diff --git a/openstack_dashboard/contrib/trove/content/databases/workflows/create_instance.py b/openstack_dashboard/contrib/trove/content/databases/workflows/create_instance.py
index aebbc0c302..7912b5aa08 100644
--- a/openstack_dashboard/contrib/trove/content/databases/workflows/create_instance.py
+++ b/openstack_dashboard/contrib/trove/content/databases/workflows/create_instance.py
@@ -102,7 +102,7 @@ class SetInstanceDetailsAction(workflows.Action):
num_datastores_with_one_version += 1
if num_datastores_with_one_version > 1:
set_initial = True
- if len(versions) > 0:
+ if versions:
# only add to choices if datastore has at least one version
version_choices = ()
for v in versions:
diff --git a/openstack_dashboard/enabled/_1740_project_database_clusters_panel.py b/openstack_dashboard/enabled/_1740_project_database_clusters_panel.py
new file mode 100644
index 0000000000..0aedc983a0
--- /dev/null
+++ b/openstack_dashboard/enabled/_1740_project_database_clusters_panel.py
@@ -0,0 +1,25 @@
+# Copyright [2015] Hewlett-Packard Development Company, L.P.
+# 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.
+
+# The slug of the panel to be added to HORIZON_CONFIG. Required.
+PANEL = 'database_clusters'
+# The slug of the dashboard the PANEL associated with. Required.
+PANEL_DASHBOARD = 'project'
+# The slug of the panel group the PANEL is associated with.
+PANEL_GROUP = 'database'
+
+# Python panel class of the PANEL to be added.
+ADD_PANEL = ('openstack_dashboard.contrib.trove.'
+ 'content.database_clusters.panel.Clusters')
diff --git a/openstack_dashboard/test/test_data/trove_data.py b/openstack_dashboard/test/test_data/trove_data.py
index a791f2acac..ed07d33165 100644
--- a/openstack_dashboard/test/test_data/trove_data.py
+++ b/openstack_dashboard/test/test_data/trove_data.py
@@ -1,4 +1,5 @@
# Copyright 2013 Rackspace Hosting.
+# Copyright 2015 HP Software, LLC
#
# 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
@@ -13,6 +14,7 @@
# under the License.
from troveclient.v1 import backups
+from troveclient.v1 import clusters
from troveclient.v1 import databases
from troveclient.v1 import datastores
from troveclient.v1 import flavors
@@ -22,6 +24,104 @@ from troveclient.v1 import users
from openstack_dashboard.test.test_data import utils
+CLUSTER_DATA_ONE = {
+ "status": "ACTIVE",
+ "id": "dfbbd9ca-b5e1-4028-adb7-f78643e17998",
+ "name": "Test Cluster",
+ "created": "2014-04-25T20:19:23",
+ "updated": "2014-04-25T20:19:23",
+ "links": [],
+ "datastore": {
+ "type": "mongodb",
+ "version": "2.6"
+ },
+ "ip": ["10.0.0.1"],
+ "instances": [
+ {
+ "id": "416b0b16-ba55-4302-bbd3-ff566032e1c1",
+ "shard_id": "5415b62f-f301-4e84-ba90-8ab0734d15a7",
+ "flavor": {
+ "id": "7",
+ "links": []
+ },
+ "volume": {
+ "size": 100
+ }
+ },
+ {
+ "id": "965ef811-7c1d-47fc-89f2-a89dfdd23ef2",
+ "shard_id": "5415b62f-f301-4e84-ba90-8ab0734d15a7",
+ "flavor": {
+ "id": "7",
+ "links": []
+ },
+ "volume": {
+ "size": 100
+ }
+ },
+ {
+ "id": "3642f41c-e8ad-4164-a089-3891bf7f2d2b",
+ "shard_id": "5415b62f-f301-4e84-ba90-8ab0734d15a7",
+ "flavor": {
+ "id": "7",
+ "links": []
+ },
+ "volume": {
+ "size": 100
+ }
+ }
+ ],
+ "task": {
+ "name": "test_task"
+ }
+}
+
+CLUSTER_DATA_TWO = {
+ "status": "ACTIVE",
+ "id": "dfbbd9ca-b5e1-4028-adb7-f78643e17998",
+ "name": "Test Cluster",
+ "created": "2014-04-25T20:19:23",
+ "updated": "2014-04-25T20:19:23",
+ "links": [],
+ "datastore": {
+ "type": "vertica",
+ "version": "7.1"
+ },
+ "ip": ["10.0.0.1"],
+ "instances": [
+ {
+ "id": "416b0b16-ba55-4302-bbd3-ff566032e1c1",
+ "flavor": {
+ "id": "7",
+ "links": []
+ },
+ "volume": {
+ "size": 100
+ }
+ },
+ {
+ "id": "965ef811-7c1d-47fc-89f2-a89dfdd23ef2",
+ "flavor": {
+ "id": "7",
+ "links": []
+ },
+ "volume": {
+ "size": 100
+ }
+ },
+ {
+ "id": "3642f41c-e8ad-4164-a089-3891bf7f2d2b",
+ "flavor": {
+ "id": "7",
+ "links": []
+ },
+ "volume": {
+ "size": 100
+ }
+ }
+ ]
+}
+
DATABASE_DATA_ONE = {
"status": "ACTIVE",
"updated": "2013-08-12T22:00:09",
@@ -130,6 +230,12 @@ DATASTORE_TWO = {
"name": "mysql"
}
+DATASTORE_MONGODB = {
+ "id": "ccb31517-c472-409d-89b4-1a13db6bdd37",
+ "links": [],
+ "name": "mongodb"
+}
+
VERSION_ONE = {
"name": "5.5",
"links": [],
@@ -171,8 +277,22 @@ FLAVOR_THREE = {
"name": "test.1"
}
+VERSION_MONGODB_2_6 = {
+ "name": "2.6",
+ "links": [],
+ "image": "c7956bb5-920e-4299-b68e-2347d830d937",
+ "active": 1,
+ "datastore": "ccb31517-c472-409d-89b4-1a13db6bdd37",
+ "packages": "2.6",
+ "id": "600a6d52-8347-4e00-8e4c-f4fa9cf96ae9"
+}
+
def data(TEST):
+ cluster1 = clusters.Cluster(clusters.Clusters(None),
+ CLUSTER_DATA_ONE)
+ cluster2 = clusters.Cluster(clusters.Clusters(None),
+ CLUSTER_DATA_TWO)
database1 = instances.Instance(instances.Instances(None),
DATABASE_DATA_ONE)
database2 = instances.Instance(instances.Instances(None),
@@ -186,18 +306,22 @@ def data(TEST):
datastore1 = datastores.Datastore(datastores.Datastores(None),
DATASTORE_ONE)
-
version1 = datastores.\
DatastoreVersion(datastores.DatastoreVersions(None),
VERSION_ONE)
- version2 = datastores.\
- DatastoreVersion(datastores.DatastoreVersions(None),
- VERSION_TWO)
flavor1 = flavors.Flavor(flavors.Flavors(None), FLAVOR_ONE)
flavor2 = flavors.Flavor(flavors.Flavors(None), FLAVOR_TWO)
flavor3 = flavors.Flavor(flavors.Flavors(None), FLAVOR_THREE)
+ datastore_mongodb = datastores.Datastore(datastores.Datastores(None),
+ DATASTORE_MONGODB)
+ version_mongodb_2_6 = datastores.\
+ DatastoreVersion(datastores.DatastoreVersions(None),
+ VERSION_MONGODB_2_6)
+ TEST.trove_clusters = utils.TestDataContainer()
+ TEST.trove_clusters.add(cluster1)
+ TEST.trove_clusters.add(cluster2)
TEST.databases = utils.TestDataContainer()
TEST.database_backups = utils.TestDataContainer()
TEST.database_users = utils.TestDataContainer()
@@ -213,7 +337,8 @@ def data(TEST):
TEST.database_user_dbs.add(user_db1)
TEST.datastores = utils.TestDataContainer()
TEST.datastores.add(datastore1)
- TEST.datastore_versions = utils.TestDataContainer()
- TEST.datastore_versions.add(version1)
- TEST.datastore_versions.add(version2)
+ TEST.datastores.add(datastore_mongodb)
TEST.database_flavors.add(flavor1, flavor2, flavor3)
+ TEST.datastore_versions = utils.TestDataContainer()
+ TEST.datastore_versions.add(version_mongodb_2_6)
+ TEST.datastore_versions.add(version1)