Merge "Moving Trove to contrib"

This commit is contained in:
Jenkins 2015-08-12 05:17:16 +00:00 committed by Gerrit Code Review
commit cc75e4f0a8
45 changed files with 3149 additions and 3 deletions

@ -0,0 +1,5 @@
from openstack_dashboard.contrib.trove.api import trove
__all__ = [
"trove"
]

@ -0,0 +1,154 @@
# Copyright 2013 Rackspace Hosting.
#
# 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.conf import settings
from troveclient.v1 import client
from openstack_dashboard.api import base
from horizon.utils import functions as utils
from horizon.utils.memoized import memoized # noqa
LOG = logging.getLogger(__name__)
@memoized
def troveclient(request):
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
trove_url = base.url_for(request, 'database')
c = client.Client(request.user.username,
request.user.token.id,
project_id=request.user.project_id,
auth_url=trove_url,
insecure=insecure,
cacert=cacert,
http_log_debug=settings.DEBUG)
c.client.auth_token = request.user.token.id
c.client.management_url = trove_url
return c
def instance_list(request, marker=None):
page_size = utils.get_page_size(request)
return troveclient(request).instances.list(limit=page_size, marker=marker)
def instance_get(request, instance_id):
return troveclient(request).instances.get(instance_id)
def instance_delete(request, instance_id):
return troveclient(request).instances.delete(instance_id)
def instance_create(request, name, volume, flavor, databases=None,
users=None, restore_point=None, nics=None,
datastore=None, datastore_version=None,
replica_of=None):
# TODO(dklyle): adding conditional 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
return troveclient(request).instances.create(
name,
flavor,
volume=volume_params,
databases=databases,
users=users,
restorePoint=restore_point,
nics=nics,
datastore=datastore,
datastore_version=datastore_version,
replica_of=replica_of)
def instance_resize_volume(request, instance_id, size):
return troveclient(request).instances.resize_volume(instance_id, size)
def instance_resize(request, instance_id, flavor_id):
return troveclient(request).instances.resize_instance(instance_id,
flavor_id)
def instance_backups(request, instance_id):
return troveclient(request).instances.backups(instance_id)
def instance_restart(request, instance_id):
return troveclient(request).instances.restart(instance_id)
def instance_detach_replica(request, instance_id):
return troveclient(request).instances.edit(instance_id,
detach_replica_source=True)
def database_list(request, instance_id):
return troveclient(request).databases.list(instance_id)
def database_delete(request, instance_id, db_name):
return troveclient(request).databases.delete(instance_id, db_name)
def backup_list(request):
return troveclient(request).backups.list()
def backup_get(request, backup_id):
return troveclient(request).backups.get(backup_id)
def backup_delete(request, backup_id):
return troveclient(request).backups.delete(backup_id)
def backup_create(request, name, instance_id, description=None,
parent_id=None):
return troveclient(request).backups.create(name, instance_id,
description, parent_id)
def flavor_list(request):
return troveclient(request).flavors.list()
def flavor_get(request, flavor_id):
return troveclient(request).flavors.get(flavor_id)
def users_list(request, instance_id):
return troveclient(request).users.list(instance_id)
def user_delete(request, instance_id, user):
return troveclient(request).users.delete(instance_id, user)
def user_list_access(request, instance_id, user):
return troveclient(request).users.list_access(instance_id, user)
def datastore_list(request):
return troveclient(request).datastores.list()
def datastore_version_list(request, datastore):
return troveclient(request).datastore_versions.list(datastore)

@ -0,0 +1,24 @@
# Copyright 2013 Rackspace Hosting
#
# 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 Backups(horizon.Panel):
name = _("Backups")
slug = 'database_backups'
permissions = ('openstack.services.database',
'openstack.services.object-store',)

@ -0,0 +1,185 @@
# Copyright 2013 Rackspace Hosting
#
# 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.template import defaultfilters as d_filters
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import tables
from horizon.utils import filters
from openstack_dashboard.contrib.trove import api
STATUS_CHOICES = (
("BUILDING", None),
("COMPLETED", True),
("DELETE_FAILED", False),
("FAILED", False),
("NEW", None),
("SAVING", None),
)
STATUS_DISPLAY_CHOICES = (
("BUILDING", pgettext_lazy("Current status of a Database Backup",
u"Building")),
("COMPLETED", pgettext_lazy("Current status of a Database Backup",
u"Completed")),
("DELETE_FAILED", pgettext_lazy("Current status of a Database Backup",
u"Delete Failed")),
("FAILED", pgettext_lazy("Current status of a Database Backup",
u"Failed")),
("NEW", pgettext_lazy("Current status of a Database Backup",
u"New")),
("SAVING", pgettext_lazy("Current status of a Database Backup",
u"Saving")),
)
class LaunchLink(tables.LinkAction):
name = "create"
verbose_name = _("Create Backup")
url = "horizon:project:database_backups:create"
classes = ("ajax-modal", "btn-create")
icon = "camera"
class RestoreLink(tables.LinkAction):
name = "restore"
verbose_name = _("Restore Backup")
url = "horizon:project:databases:launch"
classes = ("ajax-modal",)
icon = "cloud-upload"
def allowed(self, request, backup=None):
return backup.status == 'COMPLETED'
def get_link_url(self, datum):
url = reverse(self.url)
return url + '?backup=%s' % datum.id
class DownloadBackup(tables.LinkAction):
name = "download"
verbose_name = _("Download Backup")
url = 'horizon:project:containers:object_download'
classes = ("btn-download",)
def get_link_url(self, datum):
ref = datum.locationRef.split('/')
container_name = ref[5]
object_path = '/'.join(ref[6:])
return reverse(self.url,
kwargs={'container_name': container_name,
'object_path': object_path})
def allowed(self, request, datum):
return datum.status == 'COMPLETED'
class DeleteBackup(tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Backup",
u"Delete Backups",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Backup",
u"Deleted Backups",
count
)
def delete(self, request, obj_id):
api.trove.backup_delete(request, obj_id)
class UpdateRow(tables.Row):
ajax = True
def get_data(self, request, backup_id):
backup = api.trove.backup_get(request, backup_id)
try:
backup.instance = api.trove.instance_get(request,
backup.instance_id)
except Exception:
pass
return backup
def db_link(obj):
if not hasattr(obj, 'instance'):
return
if hasattr(obj.instance, 'name'):
return reverse(
'horizon:project:databases:detail',
kwargs={'instance_id': obj.instance_id})
def db_name(obj):
if not hasattr(obj, 'instance') or not hasattr(obj.instance, 'name'):
return obj.instance_id
return obj.instance.name
def get_datastore(obj):
if hasattr(obj, "datastore"):
return obj.datastore["type"]
return _("Not available")
def get_datastore_version(obj):
if hasattr(obj, "datastore"):
return obj.datastore["version"]
return _("Not available")
def is_incremental(obj):
return hasattr(obj, 'parent_id') and obj.parent_id is not None
class BackupsTable(tables.DataTable):
name = tables.Column("name",
link="horizon:project:database_backups:detail",
verbose_name=_("Name"))
datastore = tables.Column(get_datastore,
verbose_name=_("Datastore"))
datastore_version = tables.Column(get_datastore_version,
verbose_name=_("Datastore Version"))
created = tables.Column("created", verbose_name=_("Created"),
filters=[filters.parse_isotime])
instance = tables.Column(db_name, link=db_link,
verbose_name=_("Database"))
incremental = tables.Column(is_incremental,
verbose_name=_("Incremental"),
filters=(d_filters.yesno,
d_filters.capfirst))
status = tables.Column("status",
verbose_name=_("Status"),
status=True,
status_choices=STATUS_CHOICES,
display_choices=STATUS_DISPLAY_CHOICES)
class Meta(object):
name = "backups"
verbose_name = _("Backups")
status_columns = ["status"]
row_class = UpdateRow
table_actions = (LaunchLink, DeleteBackup)
row_actions = (RestoreLink, DownloadBackup, DeleteBackup)

@ -0,0 +1,4 @@
{% load i18n %}
<p>{% blocktrans %}Specify the details for the database backup.{% endblocktrans %}</p>
<p>{% blocktrans %}You can perform an incremental backup by specifying a parent backup. <strong>However,</strong> not all databases support incremental backups in which case this operation will result in an error.{% endblocktrans %}</p>

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Backup Database" %}{% endblock %}
{% block main %}
{% include 'horizon/common/_workflow.html' %}
{% endblock %}

@ -0,0 +1,74 @@
{% extends 'base.html' %}
{% load i18n sizeformat %}
{% block title %}{% trans "Backup Details" %}{% endblock %}
{% block main %}
<div class="row">
<div class="col-sm-12">
<h3>{% trans "Backup Overview" %}</h3>
<div class="status row detail">
<h4>{% trans "Information" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd>{{ backup.name }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ backup.description|linebreaksbr }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ backup.id }}</dd>
{% if backup.datastore %}
<dt>{% trans "Datastore" %}</dt>
<dd>{{ backup.datastore.type }}</dd>
<dt>{% trans "Datastore Version" %}</dt>
<dd>{{ backup.datastore.version }}</dd>
{% endif %}
<dt>{% trans "Status" %}</dt>
<dd>{{ backup.status|title }}</dd>
<dt>{% trans "Backup File Location" %}</dt>
<dd>{{ backup.locationRef }}</dd>
<dt>{% trans "Initial Volume Size" %}</dt>
<dd>{{ backup.size }} {% trans "GB" %}</dd>
<dt>{% trans "Created" %}</dt>
<dd>{{ backup.created|parse_isotime }}</dd>
<dt>{% trans "Updated" %}</dt>
<dd>{{ backup.updated|parse_isotime }}</dd>
<dt>{% trans "Backup Duration" %}</dt>
<dd>{{ backup.duration }}</dd>
</dl>
</div>
{% if backup.parent %}
<div class="status row detail">
<h4>{% trans "Incremental Backup" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt>{% trans "Parent Backup" %}</dt>
<dd>
{% url 'horizon:project:database_backups:detail' backup.parent.id as parent_url %}
<a href="{{ parent_url }}">{{ backup.parent.name }}</a>
</dd>
</dl>
</div>
{% endif %}
{% if instance %}
<div class="addresses row detail">
<h4>{% trans "Database Info" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd>{{ instance.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>
{% url 'horizon:project:databases:detail' instance.id as instance_url %}
<a href="{{ instance_url }}">{{ instance.id }}</a>
</dd>
<dt>{% trans "Status" %}</dt>
<dd>{{ instance.status|title }}</dd>
</dl>
</div>
{% endif %}
</div>
</div>
{% endblock %}

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Database Backups" %}{% endblock %}
{% block main %}
{{ table.render }}
{% endblock %}

@ -0,0 +1,182 @@
# Copyright 2013 Mirantis 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.
from django.core.urlresolvers import reverse
from django import http
from mox3.mox import IsA # noqa
from openstack_dashboard.contrib.trove import api
from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:project:database_backups:index')
BACKUP_URL = reverse('horizon:project:database_backups:create')
DETAILS_URL = reverse('horizon:project:database_backups:detail', args=['id'])
class DatabasesBackupsTests(test.TestCase):
@test.create_stubs({api.trove: ('backup_list', 'instance_get')})
def test_index(self):
api.trove.backup_list(IsA(http.HttpRequest))\
.AndReturn(self.database_backups.list())
api.trove.instance_get(IsA(http.HttpRequest),
IsA(str))\
.MultipleTimes()\
.AndReturn(self.databases.first())
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/database_backups/index.html')
@test.create_stubs({api.trove: ('backup_list',)})
def test_index_exception(self):
api.trove.backup_list(IsA(http.HttpRequest))\
.AndRaise(self.exceptions.trove)
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(
res, 'project/database_backups/index.html')
self.assertEqual(res.status_code, 200)
self.assertMessageCount(res, error=1)
@test.create_stubs({api.trove: ('instance_list',
'backup_list',
'backup_create')})
def test_launch_backup(self):
api.trove.instance_list(IsA(http.HttpRequest))\
.AndReturn(self.databases.list())
api.trove.backup_list(IsA(http.HttpRequest)) \
.AndReturn(self.database_backups.list())
database = self.databases.first()
backupName = "NewBackup"
backupDesc = "Backup Description"
api.trove.backup_create(
IsA(http.HttpRequest),
backupName,
database.id,
backupDesc,
"")
self.mox.ReplayAll()
post = {
'name': backupName,
'instance': database.id,
'description': backupDesc,
'parent': ""
}
res = self.client.post(BACKUP_URL, post)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.trove: ('instance_list', 'backup_list')})
def test_launch_backup_exception(self):
api.trove.instance_list(IsA(http.HttpRequest))\
.AndRaise(self.exceptions.trove)
api.trove.backup_list(IsA(http.HttpRequest)) \
.AndReturn(self.database_backups.list())
self.mox.ReplayAll()
res = self.client.get(BACKUP_URL)
self.assertMessageCount(res, error=1)
self.assertTemplateUsed(res,
'project/database_backups/backup.html')
@test.create_stubs({api.trove: ('instance_list',
'backup_list',
'backup_create')})
def test_launch_backup_incr(self):
api.trove.instance_list(IsA(http.HttpRequest)) \
.AndReturn(self.databases.list())
api.trove.backup_list(IsA(http.HttpRequest)) \
.AndReturn(self.database_backups.list())
database = self.databases.first()
backupName = "NewBackup"
backupDesc = "Backup Description"
backupParent = self.database_backups.first()
api.trove.backup_create(
IsA(http.HttpRequest),
backupName,
database.id,
backupDesc,
backupParent.id)
self.mox.ReplayAll()
post = {
'name': backupName,
'instance': database.id,
'description': backupDesc,
'parent': backupParent.id,
}
res = self.client.post(BACKUP_URL, post)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.trove: ('backup_get', 'instance_get')})
def test_detail_backup(self):
api.trove.backup_get(IsA(http.HttpRequest),
IsA(unicode))\
.AndReturn(self.database_backups.first())
api.trove.instance_get(IsA(http.HttpRequest),
IsA(str))\
.AndReturn(self.databases.first())
self.mox.ReplayAll()
res = self.client.get(DETAILS_URL)
self.assertTemplateUsed(res,
'project/database_backups/details.html')
@test.create_stubs({api.trove: ('backup_get',)})
def test_detail_backup_notfound(self):
api.trove.backup_get(IsA(http.HttpRequest),
IsA(unicode))\
.AndRaise(self.exceptions.trove)
self.mox.ReplayAll()
res = self.client.get(DETAILS_URL)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.trove: ('backup_get', 'instance_get')})
def test_detail_backup_incr(self):
incr_backup = self.database_backups.list()[2]
parent_backup = self.database_backups.list()[1]
api.trove.backup_get(IsA(http.HttpRequest), IsA(unicode))\
.AndReturn(incr_backup)
api.trove.backup_get(IsA(http.HttpRequest), incr_backup.parent_id) \
.AndReturn(parent_backup)
api.trove.instance_get(IsA(http.HttpRequest), IsA(str))\
.AndReturn(self.databases.list()[1])
self.mox.ReplayAll()
url = reverse('horizon:project:database_backups:detail',
args=[incr_backup.id])
res = self.client.get(url)
self.assertTemplateUsed(res, 'project/database_backups/details.html')

@ -0,0 +1,26 @@
# Copyright 2013 Rackspace Hosting
#
# 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
from django.conf.urls import url
from openstack_dashboard.contrib.trove.content.database_backups import views
urlpatterns = patterns(
'',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create$', views.BackupView.as_view(), name='create'),
url(r'^(?P<backup_id>[^/]+)/$', views.DetailView.as_view(),
name='detail'),
)

@ -0,0 +1,110 @@
# Copyright 2013 Rackspace Hosting
#
# 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 displaying database backups.
"""
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables as horizon_tables
from horizon.utils import filters
from horizon import views as horizon_views
from horizon import workflows as horizon_workflows
from openstack_dashboard.contrib.trove import api
from openstack_dashboard.contrib.trove.content.database_backups import tables
from openstack_dashboard.contrib.trove.content.database_backups \
import workflows
class IndexView(horizon_tables.DataTableView):
table_class = tables.BackupsTable
template_name = 'project/database_backups/index.html'
page_title = _("Backups")
def _get_extra_data(self, backup):
"""Apply extra info to the backup."""
instance_id = backup.instance_id
# TODO(rdopieralski) It's not clear where this attribute is supposed
# to come from. At first glance it looks like it will always be {}.
if not hasattr(self, '_instances'):
self._instances = {}
instance = self._instances.get(instance_id)
if instance is None:
try:
instance = api.trove.instance_get(self.request, instance_id)
except Exception:
instance = _('Not Found')
backup.instance = instance
return backup
def get_data(self):
# TODO(rmyers) Add pagination support after it is available
# https://blueprints.launchpad.net/trove/+spec/paginate-backup-list
try:
backups = api.trove.backup_list(self.request)
backups = map(self._get_extra_data, backups)
except Exception:
backups = []
msg = _('Error getting database backup list.')
exceptions.handle(self.request, msg)
return backups
class BackupView(horizon_workflows.WorkflowView):
workflow_class = workflows.CreateBackup
template_name = "project/database_backups/backup.html"
page_title = _("Backup Database")
def get_context_data(self, **kwargs):
context = super(BackupView, self).get_context_data(**kwargs)
context["instance_id"] = kwargs.get("instance_id")
self._instance = context['instance_id']
return context
class DetailView(horizon_views.APIView):
template_name = "project/database_backups/details.html"
page_title = _("Backup Details: {{ backup.name }}")
def get_data(self, request, context, *args, **kwargs):
backup_id = kwargs.get("backup_id")
try:
backup = api.trove.backup_get(request, backup_id)
created_at = filters.parse_isotime(backup.created)
updated_at = filters.parse_isotime(backup.updated)
backup.duration = updated_at - created_at
except Exception:
redirect = reverse('horizon:project:database_backups:index')
msg = _('Unable to retrieve details for backup: %s') % backup_id
exceptions.handle(self.request, msg, redirect=redirect)
try:
if(hasattr(backup, 'parent_id') and backup.parent_id is not None):
backup.parent = api.trove.backup_get(request, backup.parent_id)
except Exception:
redirect = reverse('horizon:project:database_backups:index')
msg = (_('Unable to retrieve details for parent backup: %s')
% backup.parent_id)
exceptions.handle(self.request, msg, redirect=redirect)
try:
instance = api.trove.instance_get(request, backup.instance_id)
except Exception:
instance = None
context['backup'] = backup
context['instance'] = instance
return context

@ -0,0 +1,3 @@
# Importing non-modules that are not used explicitly
from .create_backup import CreateBackup # noqa

@ -0,0 +1,110 @@
# Copyright 2013 Rackspace Hosting
#
# 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.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import workflows
from openstack_dashboard.contrib.trove import api
from openstack_dashboard.contrib.trove.content.databases \
import tables as project_tables
LOG = logging.getLogger(__name__)
class BackupDetailsAction(workflows.Action):
name = forms.CharField(max_length=80, label=_("Name"))
instance = forms.ChoiceField(label=_("Database Instance"))
description = forms.CharField(max_length=512, label=_("Description"),
widget=forms.TextInput(),
required=False,
help_text=_("Optional Backup Description"))
parent = forms.ChoiceField(label=_("Parent Backup"),
required=False,
help_text=_("Optional parent backup"))
class Meta(object):
name = _("Details")
help_text_template = \
"project/database_backups/_backup_details_help.html"
def populate_instance_choices(self, request, context):
LOG.info("Obtaining list of instances.")
try:
instances = api.trove.instance_list(request)
except Exception:
instances = []
msg = _("Unable to list database instances to backup.")
exceptions.handle(request, msg)
return [(i.id, i.name) for i in instances
if i.status in project_tables.ACTIVE_STATES]
def populate_parent_choices(self, request, context):
try:
backups = api.trove.backup_list(request)
choices = [(b.id, b.name) for b in backups
if b.status == 'COMPLETED']
except Exception:
choices = []
msg = _("Unable to list database backups for parent.")
exceptions.handle(request, msg)
if choices:
choices.insert(0, ("", _("Select parent backup")))
else:
choices.insert(0, ("", _("No backups available")))
return choices
class SetBackupDetails(workflows.Step):
action_class = BackupDetailsAction
contributes = ["name", "description", "instance", "parent"]
class CreateBackup(workflows.Workflow):
slug = "create_backup"
name = _("Backup Database")
finalize_button_name = _("Backup")
success_message = _('Scheduled backup "%(name)s".')
failure_message = _('Unable to launch %(count)s named "%(name)s".')
success_url = "horizon:project:database_backups:index"
default_steps = [SetBackupDetails]
def get_initial(self):
initial = super(CreateBackup, self).get_initial()
initial['instance_id']
def format_status_message(self, message):
name = self.context.get('name', 'unknown instance')
return message % {"count": _("instance"), "name": name}
def handle(self, request, context):
try:
LOG.info("Creating backup")
api.trove.backup_create(request,
context['name'],
context['instance'],
context['description'],
context['parent'])
return True
except Exception:
LOG.exception("Exception while creating backup")
msg = _('Error creating database backup.')
exceptions.handle(request, msg)
return False

@ -0,0 +1,93 @@
# Copyright 2014 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.
from django.core.urlresolvers import reverse
from django.forms import ValidationError # noqa
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard.contrib.trove import api
class ResizeVolumeForm(forms.SelfHandlingForm):
instance_id = forms.CharField(widget=forms.HiddenInput())
orig_size = forms.IntegerField(
label=_("Current Size (GB)"),
widget=forms.TextInput(attrs={'readonly': 'readonly'}),
required=False,
)
new_size = forms.IntegerField(label=_("New Size (GB)"))
def clean(self):
cleaned_data = super(ResizeVolumeForm, self).clean()
new_size = cleaned_data.get('new_size')
if new_size <= self.initial['orig_size']:
raise ValidationError(
_("New size for volume must be greater than current size."))
return cleaned_data
def handle(self, request, data):
instance = data.get('instance_id')
try:
api.trove.instance_resize_volume(request,
instance,
data['new_size'])
messages.success(request, _('Resizing volume "%s"') % instance)
except Exception as e:
redirect = reverse("horizon:project:databases:index")
exceptions.handle(request, _('Unable to resize volume. %s') %
e.message, redirect=redirect)
return True
class ResizeInstanceForm(forms.SelfHandlingForm):
instance_id = forms.CharField(widget=forms.HiddenInput())
old_flavor_name = forms.CharField(label=_("Old Flavor"),
required=False,
widget=forms.TextInput(
attrs={'readonly': 'readonly'}))
new_flavor = forms.ChoiceField(label=_("New Flavor"),
help_text=_("Choose a new instance "
"flavor."))
def __init__(self, request, *args, **kwargs):
super(ResizeInstanceForm, self).__init__(request, *args, **kwargs)
old_flavor_id = kwargs.get('initial', {}).get('old_flavor_id')
choices = kwargs.get('initial', {}).get('flavors')
# Remove current flavor from the list of flavor choices
choices = [(flavor_id, name) for (flavor_id, name) in choices
if flavor_id != old_flavor_id]
if choices:
choices.insert(0, ("", _("Select a new flavor")))
else:
choices.insert(0, ("", _("No flavors available")))
self.fields['new_flavor'].choices = choices
def handle(self, request, data):
instance = data.get('instance_id')
flavor = data.get('new_flavor')
try:
api.trove.instance_resize(request, instance, flavor)
messages.success(request, _('Resizing instance "%s"') % instance)
except Exception as e:
redirect = reverse("horizon:project:databases:index")
exceptions.handle(request, _('Unable to resize instance. %s') %
e.message, redirect=redirect)
return True

@ -0,0 +1,23 @@
# Copyright 2013 Rackspace Hosting
#
# 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 Databases(horizon.Panel):
name = _("Instances")
slug = 'databases'
permissions = ('openstack.services.database',)

@ -0,0 +1,410 @@
# Copyright 2012 Nebula, 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.
from django.core import urlresolvers
from django.template import defaultfilters as d_filters
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import tables
from horizon.templatetags import sizeformat
from horizon.utils import filters
from openstack_dashboard.contrib.trove import api
from openstack_dashboard.contrib.trove.content.database_backups \
import tables as backup_tables
ACTIVE_STATES = ("ACTIVE",)
class TerminateInstance(tables.BatchAction):
help_text = _("Terminated instances are not recoverable.")
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Terminate Instance",
u"Terminate Instances",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Scheduled termination of Instance",
u"Scheduled termination of Instances",
count
)
name = "terminate"
classes = ("btn-danger", )
icon = "remove"
def action(self, request, obj_id):
api.trove.instance_delete(request, obj_id)
class RestartInstance(tables.BatchAction):
help_text = _("Restarted instances will lose any data not"
" saved in persistent storage.")
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Restart Instance",
u"Restart Instances",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Restarted Instance",
u"Restarted Instances",
count
)
name = "restart"
classes = ('btn-danger', 'btn-reboot')
def allowed(self, request, instance=None):
return ((instance.status in ACTIVE_STATES
or instance.status == 'SHUTDOWN'))
def action(self, request, obj_id):
api.trove.instance_restart(request, obj_id)
class DetachReplica(tables.BatchAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Detach Replica",
u"Detach Replicas",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Replica Detached",
u"Replicas Detached",
count
)
name = "detach_replica"
classes = ('btn-danger', 'btn-detach-replica')
def allowed(self, request, instance=None):
return (instance.status in ACTIVE_STATES
and hasattr(instance, 'replica_of'))
def action(self, request, obj_id):
api.trove.instance_detach_replica(request, obj_id)
class DeleteUser(tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete User",
u"Delete Users",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted User",
u"Deleted Users",
count
)
def delete(self, request, obj_id):
datum = self.table.get_object_by_id(obj_id)
try:
api.trove.user_delete(request, datum.instance.id, datum.name)
except Exception:
msg = _('Error deleting database user.')
exceptions.handle(request, msg)
class DeleteDatabase(tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Database",
u"Delete Databases",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Database",
u"Deleted Databases",
count
)
def delete(self, request, obj_id):
datum = self.table.get_object_by_id(obj_id)
try:
api.trove.database_delete(request, datum.instance.id, datum.name)
except Exception:
msg = _('Error deleting database on instance.')
exceptions.handle(request, msg)
class LaunchLink(tables.LinkAction):
name = "launch"
verbose_name = _("Launch Instance")
url = "horizon:project:databases:launch"
classes = ("ajax-modal", "btn-launch")
icon = "cloud-upload"
class CreateBackup(tables.LinkAction):
name = "backup"
verbose_name = _("Create Backup")
url = "horizon:project:database_backups:create"
classes = ("ajax-modal",)
icon = "camera"
def allowed(self, request, instance=None):
return (instance.status in ACTIVE_STATES and
request.user.has_perm('openstack.services.object-store'))
def get_link_url(self, datam):
url = urlresolvers.reverse(self.url)
return url + "?instance=%s" % datam.id
class ResizeVolume(tables.LinkAction):
name = "resize_volume"
verbose_name = _("Resize Volume")
url = "horizon:project:databases:resize_volume"
classes = ("ajax-modal", "btn-resize")
def allowed(self, request, instance=None):
return instance.status in ACTIVE_STATES
def get_link_url(self, datum):
instance_id = self.table.get_object_id(datum)
return urlresolvers.reverse(self.url, args=[instance_id])
class ResizeInstance(tables.LinkAction):
name = "resize_instance"
verbose_name = _("Resize Instance")
url = "horizon:project:databases:resize_instance"
classes = ("ajax-modal", "btn-resize")
def allowed(self, request, instance=None):
return ((instance.status in ACTIVE_STATES
or instance.status == 'SHUTOFF'))
def get_link_url(self, datum):
instance_id = self.table.get_object_id(datum)
return urlresolvers.reverse(self.url, args=[instance_id])
class UpdateRow(tables.Row):
ajax = True
def get_data(self, request, instance_id):
instance = api.trove.instance_get(request, instance_id)
try:
flavor_id = instance.flavor['id']
instance.full_flavor = api.trove.flavor_get(request, flavor_id)
except Exception:
pass
instance.host = get_host(instance)
return instance
def get_datastore(instance):
if hasattr(instance, "datastore"):
return instance.datastore["type"]
return _("Not available")
def get_datastore_version(instance):
if hasattr(instance, "datastore"):
return instance.datastore["version"]
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")
def get_size(instance):
if hasattr(instance, "full_flavor"):
size_string = _("%(name)s | %(RAM)s RAM")
vals = {'name': instance.full_flavor.name,
'RAM': sizeformat.mb_float_format(instance.full_flavor.ram)}
return size_string % vals
return _("Not available")
def get_volume_size(instance):
if hasattr(instance, "volume"):
return sizeformat.diskgbformat(instance.volume.get("size"))
return _("Not available")
def get_databases(user):
if hasattr(user, "access"):
databases = [db.name for db in user.access]
databases.sort()
return ', '.join(databases)
return _("-")
class InstancesTable(tables.DataTable):
STATUS_CHOICES = (
("ACTIVE", True),
("BLOCKED", True),
("BUILD", None),
("FAILED", False),
("REBOOT", None),
("RESIZE", None),
("BACKUP", None),
("SHUTDOWN", False),
("ERROR", False),
("RESTART_REQUIRED", None),
)
STATUS_DISPLAY_CHOICES = (
("ACTIVE", pgettext_lazy("Current status of a Database Instance",
u"Active")),
("BLOCKED", pgettext_lazy("Current status of a Database Instance",
u"Blocked")),
("BUILD", pgettext_lazy("Current status of a Database Instance",
u"Build")),
("FAILED", pgettext_lazy("Current status of a Database Instance",
u"Failed")),
("REBOOT", pgettext_lazy("Current status of a Database Instance",
u"Reboot")),
("RESIZE", pgettext_lazy("Current status of a Database Instance",
u"Resize")),
("BACKUP", pgettext_lazy("Current status of a Database Instance",
u"Backup")),
("SHUTDOWN", pgettext_lazy("Current status of a Database Instance",
u"Shutdown")),
("ERROR", pgettext_lazy("Current status of a Database Instance",
u"Error")),
("RESTART_REQUIRED",
pgettext_lazy("Current status of a Database Instance",
u"Restart Required")),
)
name = tables.Column("name",
link="horizon:project:databases:detail",
verbose_name=_("Instance Name"))
datastore = tables.Column(get_datastore,
verbose_name=_("Datastore"))
datastore_version = tables.Column(get_datastore_version,
verbose_name=_("Datastore Version"))
host = tables.Column(get_host, verbose_name=_("Host"))
size = tables.Column(get_size,
verbose_name=_("Size"),
attrs={'data-type': 'size'})
volume = tables.Column(get_volume_size,
verbose_name=_("Volume Size"),
attrs={'data-type': 'size'})
status = tables.Column("status",
verbose_name=_("Status"),
status=True,
status_choices=STATUS_CHOICES,
display_choices=STATUS_DISPLAY_CHOICES)
class Meta(object):
name = "databases"
verbose_name = _("Instances")
status_columns = ["status"]
row_class = UpdateRow
table_actions = (LaunchLink, TerminateInstance)
row_actions = (CreateBackup,
ResizeVolume,
ResizeInstance,
RestartInstance,
DetachReplica,
TerminateInstance)
class UsersTable(tables.DataTable):
name = tables.Column("name", verbose_name=_("User Name"))
host = tables.Column("host", verbose_name=_("Allowed Host"))
databases = tables.Column(get_databases, verbose_name=_("Databases"))
class Meta(object):
name = "users"
verbose_name = _("Users")
table_actions = [DeleteUser]
row_actions = [DeleteUser]
def get_object_id(self, datum):
return datum.name
class DatabaseTable(tables.DataTable):
name = tables.Column("name", verbose_name=_("Database Name"))
class Meta(object):
name = "databases"
verbose_name = _("Databases")
table_actions = [DeleteDatabase]
row_actions = [DeleteDatabase]
def get_object_id(self, datum):
return datum.name
def is_incremental(obj):
return hasattr(obj, 'parent_id') and obj.parent_id is not None
class InstanceBackupsTable(tables.DataTable):
name = tables.Column("name",
link="horizon:project:database_backups:detail",
verbose_name=_("Name"))
created = tables.Column("created", verbose_name=_("Created"),
filters=[filters.parse_isotime])
location = tables.Column(lambda obj: _("Download"),
link=lambda obj: obj.locationRef,
verbose_name=_("Backup File"))
incremental = tables.Column(is_incremental,
verbose_name=_("Incremental"),
filters=(d_filters.yesno,
d_filters.capfirst))
status = tables.Column(
"status",
verbose_name=_("Status"),
status=True,
status_choices=backup_tables.STATUS_CHOICES,
display_choices=backup_tables.STATUS_DISPLAY_CHOICES)
class Meta(object):
name = "backups"
verbose_name = _("Backups")
status_columns = ["status"]
row_class = UpdateRow
table_actions = (backup_tables.LaunchLink, backup_tables.DeleteBackup)
row_actions = (backup_tables.RestoreLink, backup_tables.DeleteBackup)

@ -0,0 +1,127 @@
# Copyright 2013 Rackspace Hosting
#
# 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 import settings
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.databases import tables
class OverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
def get_context_data(self, request):
return {"instance": self.tab_group.kwargs['instance']}
def get_template_name(self, request):
instance = self.tab_group.kwargs['instance']
template_file = ('project/databases/_detail_overview_%s.html'
% instance.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/databases/_detail_overview.html')
class UserTab(tabs.TableTab):
table_classes = [tables.UsersTable]
name = _("Users")
slug = "users_tab"
instance = None
template_name = "horizon/common/_detail_table.html"
preload = False
def get_users_data(self):
instance = self.tab_group.kwargs['instance']
try:
data = api.trove.users_list(self.request, instance.id)
for user in data:
user.instance = instance
user.access = api.trove.user_list_access(self.request,
instance.id,
user.name)
except Exception:
msg = _('Unable to get user data.')
exceptions.handle(self.request, msg)
data = []
return data
def allowed(self, request):
perms = getattr(settings, 'TROVE_ADD_USER_PERMS', [])
if perms:
return request.user.has_perms(perms)
return True
class DatabaseTab(tabs.TableTab):
table_classes = [tables.DatabaseTable]
name = _("Databases")
slug = "database_tab"
instance = None
template_name = "horizon/common/_detail_table.html"
preload = False
def get_databases_data(self):
instance = self.tab_group.kwargs['instance']
try:
data = api.trove.database_list(self.request, instance.id)
add_instance = lambda d: setattr(d, 'instance', instance)
map(add_instance, data)
except Exception:
msg = _('Unable to get databases data.')
exceptions.handle(self.request, msg)
data = []
return data
def allowed(self, request):
perms = getattr(settings, 'TROVE_ADD_DATABASE_PERMS', [])
if perms:
return request.user.has_perms(perms)
return True
class BackupsTab(tabs.TableTab):
table_classes = [tables.InstanceBackupsTable]
name = _("Backups")
slug = "backups_tab"
instance = None
template_name = "horizon/common/_detail_table.html"
preload = False
def get_backups_data(self):
instance = self.tab_group.kwargs['instance']
try:
data = api.trove.instance_backups(self.request, instance.id)
except Exception:
msg = _('Unable to get database backup data.')
exceptions.handle(self.request, msg)
data = []
return data
def allowed(self, request):
return request.user.has_perm('openstack.services.object-store')
class InstanceDetailTabs(tabs.TabGroup):
slug = "instance_details"
tabs = (OverviewTab, UserTab, DatabaseTab, BackupsTab)
sticky = True

@ -0,0 +1,69 @@
{% load i18n sizeformat %}
{% load url from future %}
<h3>{% trans "Instance Overview" %}</h3>
<div class="status row detail">
<h4>{% trans "Information" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd>{{ instance.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ instance.id }}</dd>
<dt>{% trans "Datastore" %}</dt>
<dd>{{ instance.datastore.type }}</dd>
<dt>{% trans "Datastore Version" %}</dt>
<dd>{{ instance.datastore.version }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>{{ instance.status|title }}</dd>
</dl>
</div>
<div class="specs row detail">
<h4>{% trans "Specs" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt>{% trans "Flavor" %}</dt>
<dd>{{ instance.full_flavor.name }}</dd>
<dt>{% trans "RAM" %}</dt>
<dd>{{ instance.full_flavor.ram|mb_float_format }}</dd>
{% if instance.volume %}
<dt>{% trans "Volume Size" %}</dt>
<dd>{{ instance.volume.size|diskgbformat }}</dd>
{% endif %}
<dt>{% trans "Created" %}</dt>
<dd>{{ instance.created|parse_isotime }}</dd>
<dt>{% trans "Updated" %}</dt>
<dd>{{ instance.updated|parse_isotime }}</dd>
</dl>
</div>
{% block connection_info %}
{% endblock %}
{% if instance.replica_of or instance.replicas %}
<div class="specs row detail">
<h4>{% trans "Replication" %}</h4>
<hr class="header_rule">
<dl>
{% if instance.replica_of %}
<dt>{% trans "Is a Replica Of" %}</dt>
<dd>
{% url 'horizon:project:databases:detail' instance.replica_of.id as instance_url %}
<a href="{{ instance_url }}">{{ instance.replica_of.id }}</a>
</dd>
{% endif %}
{% if instance.replicas %}
<dt>{% trans "Replicas" %}</dt>
{% for replica in instance.replicas %}
<dd>
{% url 'horizon:project:databases:detail' replica.id as instance_url %}
<a href="{{ instance_url }}">{{ replica.id }}</a>
</dd>
{% endfor %}
{% endif %}
</dl>
</div>
{% endif %}

@ -0,0 +1,23 @@
{% extends "project/databases/_detail_overview.html" %}
{% load i18n sizeformat %}
{% block connection_info %}
<div class="addresses row detail">
<h4>{% trans "Connection Info" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
{% with instance.host as host %}
<dt>{% trans "Host" %}</dt>
{% if not host %}
<dd>{% trans "Not Assigned" %}</dd>
{% else %}
<dd>{{ host }}</dd>
<dt>{% trans "Database Port" %}</dt>
<dd>9160</dd>
<dt>{% trans "Connection Examples" %}</dt>
<dd>cqlsh {{ host }} 9160</dd>
{% endif %} <!-- ends else block -->
{% endwith %}
</dl>
</div>
{% endblock %}

@ -0,0 +1,21 @@
{% extends "project/databases/_detail_overview.html" %}
{% load i18n sizeformat %}
{% block connection_info %}
<div class="addresses row detail">
<h4>{% trans "Connection Info" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
{% with instance.host as host %}
<dt>{% trans "Host" %}</dt>
{% if not host %}
<dd>{% trans "Not Assigned" %}</dd>
{% else %}
<dd>{{ host }}</dd>
<dt>{% trans "Connection Examples" %}</dt>
<dd>http://{{ host }}:8091</dd>
{% endif %} <!-- ends else block -->
{% endwith %}
</dl>
</div>
{% endblock %}

@ -0,0 +1,24 @@
{% extends "project/databases/_detail_overview.html" %}
{% load i18n sizeformat %}
{% block connection_info %}
<div class="addresses row detail">
<h4>{% trans "Connection Info" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
{% with instance.host as host %}
<dt>{% trans "Host" %}</dt>
{% if not host %}
<dd>{% trans "Not Assigned" %}</dd>
{% else %}
<dd>{{ host }}</dd>
<dt>{% trans "Database Port" %}</dt>
<dd>27017</dd>
<dt>{% trans "Connection Examples" %}</dt>
<dd>mongo --host {{ host }}</dd>
<dd>mongodb://[{% trans "USERNAME" %}:{% trans "PASSWORD" %}@]{{ host }}:27017/{% trans "DATABASE" %}</dd>
{% endif %} <!-- ends else block -->
{% endwith %}
</dl>
</div>
{% endblock %}

@ -0,0 +1,24 @@
{% extends "project/databases/_detail_overview.html" %}
{% load i18n sizeformat %}
{% block connection_info %}
<div class="addresses row detail">
<h4>{% trans "Connection Info" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
{% with instance.host as host %}
<dt>{% trans "Host" %}</dt>
{% if not host %}
<dd>{% trans "Not Assigned" %}</dd>
{% else %}
<dd>{{ host }}</dd>
<dt>{% trans "Database Port" %}</dt>
<dd>3306</dd>
<dt>{% trans "Connection Examples" %}</dt>
<dd>mysql -h {{ host }} -u {% trans "USERNAME" %} -p</dd>
<dd>mysql://{% trans "USERNAME" %}:{% trans "PASSWORD" %}@{{ host }}:3306/{% trans "DATABASE"%}</dd>
{% endif %} <!-- ends else block -->
{% endwith %}
</dl>
</div>
{% endblock %}

@ -0,0 +1,21 @@
{% extends "project/databases/_detail_overview.html" %}
{% load i18n sizeformat %}
{% block connection_info %}
<div class="addresses row detail">
<h4>{% trans "Connection Info" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
{% with instance.host as host %}
<dt>{% trans "Host" %}</dt>
{% if not host %}
<dd>{% trans "Not Assigned" %}</dd>
{% else %}
<dd>{{ host }}</dd>
<dt>{% trans "Connection Examples" %}</dt>
<dd>redis-cli -h {{ host }}</dd>
{% endif %} <!-- ends else block -->
{% endwith %}
</dl>
</div>
{% endblock %}

@ -0,0 +1,3 @@
{% load i18n %}
<p>{% blocktrans %}Optionally choose to create this database using a previous backup, or as a replica of another database instance.{% endblocktrans %}</p>

@ -0,0 +1,4 @@
{% load i18n %}
<p>{% blocktrans %}Specify the details for launching an instance.{% endblocktrans %}</p>
<p>{% blocktrans %}<strong>Please note:</strong> The value specified in the Volume Size field should be greater than 0, however, some configurations do not support specifying volume size. If specifying the volume size results in an error stating volume support is not enabled, enter 0.{% endblocktrans %}</p>

@ -0,0 +1,18 @@
{% load i18n %}
<h4>{% trans "Initial Databases" %}</h4>
<p>{% trans "Optionally provide a comma separated list of databases to create:" %}</p>
<pre>database1, database2, database3</pre>
<h4>{% trans "Initial Admin User" %}</h4>
<p>{% blocktrans %}Create an optional initial user.
This user will have access to all databases you create.{% endblocktrans %}</p>
<ul>
<li><strong>{% trans "Username (required)" %}</strong></li>
<li><strong>{% trans "Password (required)" %}</strong></li>
<li><strong>{% trans "Allowed Host (optional)" %}</strong>
{% blocktrans %}Allow the user to connect from this host
only. If not provided this user will be allowed to connect from anywhere.
{% endblocktrans %}</li>
</ul>

@ -0,0 +1,9 @@
{% load i18n %}
<p>
{% blocktrans %}
Move networks from 'Available Networks' to 'Selected Networks' by
clicking the button, or dragging and dropping. You can change the
NIC order by dragging and dropping as well.
{% endblocktrans %}
</p>

@ -0,0 +1,46 @@
{% load i18n %}
<noscript><h3>{{ step }}</h3></noscript>
<table class="table-fixed" id="networkListSortContainer">
<tbody>
<tr>
<td class="actions">
<label id="selected_network_label">{% trans "Selected networks" %}</label>
<ul id="selected_network" class="networklist">
</ul>
<label>{% trans "Available networks" %}</label>
<ul id="available_network" class="networklist">
</ul>
</td>
<td class="help_text">
{% include "project/databases/_launch_network_help.html" %}
</td>
</tr>
</tbody>
</table>
<table class="table-fixed" id="networkListIdContainer">
<tbody>
<tr>
<td class="actions">
<div id="networkListId">
{% include "horizon/common/_form_fields.html" %}
</div>
</td>
<td class="help_text">
{{ step.get_help_text }}
</td>
</tr>
</tbody>
</table>
<script>
if (typeof $ !== 'undefined') {
horizon.instances.workflow_init($(".workflow"));
} else {
addHorizonLoadEvent(function() {
horizon.instances.workflow_init($(".workflow"));
});
}
</script>

@ -0,0 +1,26 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}resize_instance_form{% endblock %}
{% block form_action %}{% url "horizon:project:databases:resize_instance" instance_id %}{% endblock %}
{% block modal_id %}resize_instance_modal{% endblock %}
{% block modal-header %}{% trans "Resize Database Instance" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<p>{% blocktrans %}Specify a new flavor for the database instance.{% endblocktrans %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Resize Database Instance" %}" />
<a href="{% url "horizon:project:databases:index" %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

@ -0,0 +1,27 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}resize_volume_form{% endblock %}
{% block form_action %}{% url "horizon:project:databases:resize_volume" instance_id %}{% endblock %}
{% block modal_id %}resize_volume_modal{% endblock %}
{% block modal-header %}{% trans "Resize Database Volume" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<p>{% blocktrans %}Specify the new volume size for the database instance.{% endblocktrans %}</p>
<p>{% blocktrans %}<strong>Please note:</strong> The new value must be greater than the existing volume size.{% endblocktrans %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Resize Database Volume" %}" />
<a href="{% url "horizon:project:databases:index" %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Instances" %}{% endblock %}
{% block main %}
{{ table.render }}
{% endblock %}

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Launch Instance" %}{% endblock %}
{% block main %}
{% include 'horizon/common/_workflow.html' %}
{% endblock %}

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Resize Database Instance" %}{% endblock %}
{% block main %}
{% include "project/databases/_resize_instance.html" %}
{% endblock %}

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Resize Database Volume" %}{% endblock %}
{% block main %}
{% include "project/databases/_resize_volume.html" %}
{% endblock %}

@ -0,0 +1,543 @@
# Copyright 2013 Mirantis Inc.
# Copyright 2013 Rackspace Hosting.
#
# 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
import django
from django.core.urlresolvers import reverse
from django import http
from django.utils import unittest
from mox3.mox import IsA # noqa
from horizon import exceptions
from openstack_dashboard import api as dash_api
from openstack_dashboard.contrib.trove import api
from openstack_dashboard.test import helpers as test
from troveclient import common
INDEX_URL = reverse('horizon:project:databases:index')
LAUNCH_URL = reverse('horizon:project:databases:launch')
DETAILS_URL = reverse('horizon:project:databases:detail', args=['id'])
class DatabaseTests(test.TestCase):
@test.create_stubs(
{api.trove: ('instance_list', 'flavor_list')})
def test_index(self):
# Mock database instances
databases = common.Paginated(self.databases.list())
api.trove.instance_list(IsA(http.HttpRequest), marker=None)\
.AndReturn(databases)
# Mock flavors
api.trove.flavor_list(IsA(http.HttpRequest))\
.AndReturn(self.flavors.list())
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/databases/index.html')
# Check the Host column displaying ip or hostname
self.assertContains(res, '10.0.0.3')
self.assertContains(res, 'trove.instance-2.com')
@test.create_stubs(
{api.trove: ('instance_list', 'flavor_list')})
def test_index_flavor_exception(self):
# Mock database instances
databases = common.Paginated(self.databases.list())
api.trove.instance_list(IsA(http.HttpRequest), marker=None)\
.AndReturn(databases)
# Mock flavors
api.trove.flavor_list(IsA(http.HttpRequest))\
.AndRaise(self.exceptions.trove)
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/databases/index.html')
self.assertMessageCount(res, error=1)
@test.create_stubs(
{api.trove: ('instance_list',)})
def test_index_list_exception(self):
# Mock database instances
api.trove.instance_list(IsA(http.HttpRequest), marker=None)\
.AndRaise(self.exceptions.trove)
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/databases/index.html')
self.assertMessageCount(res, error=1)
@test.create_stubs(
{api.trove: ('instance_list', 'flavor_list')})
def test_index_pagination(self):
# Mock database instances
databases = self.databases.list()
last_record = databases[1]
databases = common.Paginated(databases, next_marker="foo")
api.trove.instance_list(IsA(http.HttpRequest), marker=None)\
.AndReturn(databases)
# Mock flavors
api.trove.flavor_list(IsA(http.HttpRequest))\
.AndReturn(self.flavors.list())
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/databases/index.html')
self.assertContains(
res, 'marker=' + last_record.id)
@test.create_stubs(
{api.trove: ('instance_list', 'flavor_list')})
def test_index_flavor_list_exception(self):
# Mocking instances.
databases = common.Paginated(self.databases.list())
api.trove.instance_list(
IsA(http.HttpRequest),
marker=None,
).AndReturn(databases)
# Mocking flavor list with raising an exception.
api.trove.flavor_list(
IsA(http.HttpRequest),
).AndRaise(self.exceptions.trove)
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/databases/index.html')
self.assertMessageCount(res, error=1)
@test.create_stubs({
api.trove: ('flavor_list', 'backup_list',
'datastore_list', 'datastore_version_list',
'instance_list'),
dash_api.neutron: ('network_list',)})
def test_launch_instance(self):
api.trove.flavor_list(IsA(http.HttpRequest)).AndReturn(
self.flavors.list())
api.trove.backup_list(IsA(http.HttpRequest)).AndReturn(
self.database_backups.list())
api.trove.instance_list(IsA(http.HttpRequest)).AndReturn(
self.databases.list())
# Mock datastores
api.trove.datastore_list(IsA(http.HttpRequest)).AndReturn(
self.datastores.list())
# Mock datastore versions
api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str)).\
AndReturn(self.datastore_versions.list())
dash_api.neutron.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id,
shared=False).AndReturn(
self.networks.list()[:1])
dash_api.neutron.network_list(IsA(http.HttpRequest),
shared=True).AndReturn(
self.networks.list()[1:])
self.mox.ReplayAll()
res = self.client.get(LAUNCH_URL)
self.assertTemplateUsed(res, 'project/databases/launch.html')
# django 1.7 and later does not handle the thrown Http302
# exception well enough.
# TODO(mrunge): re-check when django-1.8 is stable
@unittest.skipIf(django.VERSION >= (1, 7, 0),
'Currently skipped with Django >= 1.7')
@test.create_stubs({api.trove: ('flavor_list',)})
def test_launch_instance_exception_on_flavors(self):
trove_exception = self.exceptions.nova
api.trove.flavor_list(IsA(http.HttpRequest)).AndRaise(trove_exception)
self.mox.ReplayAll()
toSuppress = ["openstack_dashboard.dashboards.project.databases."
"workflows.create_instance",
"horizon.workflows.base"]
# 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:
with self.assertRaises(exceptions.Http302):
self.client.get(LAUNCH_URL)
finally:
# Restore the previous log levels
for (log, level) in loggers:
log.setLevel(level)
@test.create_stubs({
api.trove: ('flavor_list', 'backup_list', 'instance_create',
'datastore_list', 'datastore_version_list',
'instance_list'),
dash_api.neutron: ('network_list',)})
def test_create_simple_instance(self):
api.trove.flavor_list(IsA(http.HttpRequest)).AndReturn(
self.flavors.list())
api.trove.backup_list(IsA(http.HttpRequest)).AndReturn(
self.database_backups.list())
api.trove.instance_list(IsA(http.HttpRequest)).AndReturn(
self.databases.list())
# Mock datastores
api.trove.datastore_list(IsA(http.HttpRequest))\
.AndReturn(self.datastores.list())
# Mock datastore versions
api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
.AndReturn(self.datastore_versions.list())
dash_api.neutron.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id,
shared=False).AndReturn(
self.networks.list()[:1])
dash_api.neutron.network_list(IsA(http.HttpRequest),
shared=True).AndReturn(
self.networks.list()[1:])
nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
# Actual create database call
api.trove.instance_create(
IsA(http.HttpRequest),
IsA(unicode),
IsA(int),
IsA(unicode),
databases=None,
datastore=IsA(unicode),
datastore_version=IsA(unicode),
restore_point=None,
replica_of=None,
users=None,
nics=nics).AndReturn(self.databases.first())
self.mox.ReplayAll()
post = {
'name': "MyDB",
'volume': '1',
'flavor': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'network': self.networks.first().id,
'datastore': 'mysql,5.5',
}
res = self.client.post(LAUNCH_URL, post)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({
api.trove: ('flavor_list', 'backup_list', 'instance_create',
'datastore_list', 'datastore_version_list',
'instance_list'),
dash_api.neutron: ('network_list',)})
def test_create_simple_instance_exception(self):
trove_exception = self.exceptions.nova
api.trove.flavor_list(IsA(http.HttpRequest)).AndReturn(
self.flavors.list())
api.trove.backup_list(IsA(http.HttpRequest)).AndReturn(
self.database_backups.list())
api.trove.instance_list(IsA(http.HttpRequest)).AndReturn(
self.databases.list())
# Mock datastores
api.trove.datastore_list(IsA(http.HttpRequest))\
.AndReturn(self.datastores.list())
# Mock datastore versions
api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
.AndReturn(self.datastore_versions.list())
dash_api.neutron.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id,
shared=False).AndReturn(
self.networks.list()[:1])
dash_api.neutron.network_list(IsA(http.HttpRequest),
shared=True).AndReturn(
self.networks.list()[1:])
nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
# Actual create database call
api.trove.instance_create(
IsA(http.HttpRequest),
IsA(unicode),
IsA(int),
IsA(unicode),
databases=None,
datastore=IsA(unicode),
datastore_version=IsA(unicode),
restore_point=None,
replica_of=None,
users=None,
nics=nics).AndRaise(trove_exception)
self.mox.ReplayAll()
post = {
'name': "MyDB",
'volume': '1',
'flavor': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'network': self.networks.first().id,
'datastore': 'mysql,5.5',
}
res = self.client.post(LAUNCH_URL, post)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs(
{api.trove: ('instance_get', 'flavor_get',)})
def _test_details(self, database, with_designate=False):
api.trove.instance_get(IsA(http.HttpRequest), IsA(unicode))\
.AndReturn(database)
api.trove.flavor_get(IsA(http.HttpRequest), IsA(str))\
.AndReturn(self.flavors.first())
self.mox.ReplayAll()
res = self.client.get(DETAILS_URL)
self.assertTemplateUsed(res, 'project/databases/detail.html')
if with_designate:
self.assertContains(res, database.hostname)
else:
self.assertContains(res, database.ip[0])
def test_details_with_ip(self):
database = self.databases.first()
self._test_details(database, with_designate=False)
def test_details_with_hostname(self):
database = self.databases.list()[1]
self._test_details(database, with_designate=True)
@test.create_stubs(
{api.trove: ('instance_get', 'flavor_get', 'users_list',
'user_list_access', 'user_delete')})
def test_user_delete(self):
database = self.databases.first()
user = self.database_users.first()
user_db = self.database_user_dbs.first()
database_id = database.id
# Instead of using the user's ID, the api uses the user's name. BOOO!
user_id = user.name
# views.py: DetailView.get_data
api.trove.instance_get(IsA(http.HttpRequest), IsA(unicode))\
.AndReturn(database)
api.trove.flavor_get(IsA(http.HttpRequest), IsA(str))\
.AndReturn(self.flavors.first())
# tabs.py: UserTab.get_user_data
api.trove.users_list(IsA(http.HttpRequest),
database_id).AndReturn([user])
api.trove.user_list_access(IsA(http.HttpRequest),
database_id,
user_id).AndReturn([user_db])
# tables.py: DeleteUser.delete
api.trove.user_delete(IsA(http.HttpRequest),
database_id,
user_id).AndReturn(None)
self.mox.ReplayAll()
details_url = reverse('horizon:project:databases:detail',
args=[database_id])
url = details_url + '?tab=instance_details__users_tab'
action_string = u"users__delete__%s" % user_id
form_data = {'action': action_string}
res = self.client.post(url, form_data)
self.assertRedirectsNoFollow(res, url)
@test.create_stubs({
api.trove: ('instance_get', 'instance_resize_volume')})
def test_resize_volume(self):
database = self.databases.first()
database_id = database.id
database_size = database.volume.get('size')
# views.py: DetailView.get_data
api.trove.instance_get(IsA(http.HttpRequest), IsA(unicode))\
.AndReturn(database)
# forms.py: ResizeVolumeForm.handle
api.trove.instance_resize_volume(IsA(http.HttpRequest),
database_id,
IsA(int)).AndReturn(None)
self.mox.ReplayAll()
url = reverse('horizon:project:databases:resize_volume',
args=[database_id])
post = {
'instance_id': database_id,
'orig_size': database_size,
'new_size': database_size + 1,
}
res = self.client.post(url, post)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.trove: ('instance_get', )})
def test_resize_volume_bad_value(self):
database = self.databases.first()
database_id = database.id
database_size = database.volume.get('size')
# views.py: DetailView.get_data
api.trove.instance_get(IsA(http.HttpRequest), IsA(unicode))\
.AndReturn(database)
self.mox.ReplayAll()
url = reverse('horizon:project:databases:resize_volume',
args=[database_id])
post = {
'instance_id': database_id,
'orig_size': database_size,
'new_size': database_size,
}
res = self.client.post(url, post)
self.assertContains(
res, "New size for volume must be greater than current size.")
@test.create_stubs(
{api.trove: ('instance_get',
'flavor_list')})
def test_resize_instance_get(self):
database = self.databases.first()
# views.py: DetailView.get_data
api.trove.instance_get(IsA(http.HttpRequest), database.id)\
.AndReturn(database)
api.trove.flavor_list(IsA(http.HttpRequest)).\
AndReturn(self.database_flavors.list())
self.mox.ReplayAll()
url = reverse('horizon:project:databases:resize_instance',
args=[database.id])
res = self.client.get(url)
self.assertTemplateUsed(res, 'project/databases/resize_instance.html')
option = '<option value="%s">%s</option>'
for flavor in self.database_flavors.list():
if flavor.id == database.flavor['id']:
self.assertNotContains(res, option % (flavor.id, flavor.name))
else:
self.assertContains(res, option % (flavor.id, flavor.name))
@test.create_stubs(
{api.trove: ('instance_get',
'flavor_list',
'instance_resize')})
def test_resize_instance(self):
database = self.databases.first()
# views.py: DetailView.get_data
api.trove.instance_get(IsA(http.HttpRequest), database.id)\
.AndReturn(database)
api.trove.flavor_list(IsA(http.HttpRequest)).\
AndReturn(self.database_flavors.list())
old_flavor = self.database_flavors.list()[0]
new_flavor = self.database_flavors.list()[1]
api.trove.instance_resize(IsA(http.HttpRequest),
database.id,
new_flavor.id).AndReturn(None)
self.mox.ReplayAll()
url = reverse('horizon:project:databases:resize_instance',
args=[database.id])
post = {
'instance_id': database.id,
'old_flavor_name': old_flavor.name,
'old_flavor_id': old_flavor.id,
'new_flavor': new_flavor.id
}
res = self.client.post(url, post)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({
api.trove: ('flavor_list', 'backup_list', 'instance_create',
'datastore_list', 'datastore_version_list',
'instance_list', 'instance_get'),
dash_api.neutron: ('network_list',)})
def test_create_replica_instance(self):
api.trove.flavor_list(IsA(http.HttpRequest)).AndReturn(
self.flavors.list())
api.trove.backup_list(IsA(http.HttpRequest)).AndReturn(
self.database_backups.list())
api.trove.instance_list(IsA(http.HttpRequest)).AndReturn(
self.databases.list())
api.trove.datastore_list(IsA(http.HttpRequest))\
.AndReturn(self.datastores.list())
api.trove.datastore_version_list(IsA(http.HttpRequest),
IsA(str))\
.AndReturn(self.datastore_versions.list())
dash_api.neutron.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id,
shared=False).\
AndReturn(self.networks.list()[:1])
dash_api.neutron.network_list(IsA(http.HttpRequest),
shared=True).\
AndReturn(self.networks.list()[1:])
nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
api.trove.instance_get(IsA(http.HttpRequest), IsA(unicode))\
.AndReturn(self.databases.first())
# Actual create database call
api.trove.instance_create(
IsA(http.HttpRequest),
IsA(unicode),
IsA(int),
IsA(unicode),
databases=None,
datastore=IsA(unicode),
datastore_version=IsA(unicode),
restore_point=None,
replica_of=self.databases.first().id,
users=None,
nics=nics).AndReturn(self.databases.first())
self.mox.ReplayAll()
post = {
'name': "MyDB",
'volume': '1',
'flavor': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'network': self.networks.first().id,
'datastore': 'mysql,5.5',
'initial_state': 'master',
'master': self.databases.first().id
}
res = self.client.post(LAUNCH_URL, post)
self.assertRedirectsNoFollow(res, INDEX_URL)

@ -0,0 +1,33 @@
# Copyright 2013 Rackspace Hosting
#
# 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
from django.conf.urls import url
from openstack_dashboard.contrib.trove.content.databases import views
INSTANCES = r'^(?P<instance_id>[^/]+)/%s$'
urlpatterns = patterns(
'',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^launch$', views.LaunchInstanceView.as_view(), name='launch'),
url(INSTANCES % '', views.DetailView.as_view(), name='detail'),
url(INSTANCES % 'resize_volume', views.ResizeVolumeView.as_view(),
name='resize_volume'),
url(INSTANCES % 'resize_instance', views.ResizeInstanceView.as_view(),
name='resize_instance')
)

@ -0,0 +1,221 @@
# Copyright 2013 Rackspace Hosting
#
# 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 instances.
"""
import logging
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.utils.datastructures import SortedDict
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 horizon import workflows as horizon_workflows
from openstack_dashboard.contrib.trove import api
from openstack_dashboard.contrib.trove.content.databases import forms
from openstack_dashboard.contrib.trove.content.databases import tables
from openstack_dashboard.contrib.trove.content.databases import tabs
from openstack_dashboard.contrib.trove.content.databases import workflows
from openstack_dashboard.dashboards.project.instances \
import utils as instance_utils
LOG = logging.getLogger(__name__)
class IndexView(horizon_tables.DataTableView):
table_class = tables.InstancesTable
template_name = 'project/databases/index.html'
page_title = _("Instances")
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 SortedDict((unicode(flavor.id), flavor) for flavor in flavors)
def _extra_data(self, instance):
flavor = self.get_flavors().get(instance.flavor["id"])
if flavor is not None:
instance.full_flavor = flavor
instance.host = tables.get_host(instance)
return instance
def get_data(self):
marker = self.request.GET.get(
tables.InstancesTable._meta.pagination_param)
# Gather our instances
try:
instances = api.trove.instance_list(self.request, marker=marker)
self._more = instances.next or False
except Exception:
self._more = False
instances = []
msg = _('Unable to retrieve database instances.')
exceptions.handle(self.request, msg)
map(self._extra_data, instances)
return instances
class LaunchInstanceView(horizon_workflows.WorkflowView):
workflow_class = workflows.LaunchInstance
template_name = "project/databases/launch.html"
page_title = _("Launch Database")
def get_initial(self):
initial = super(LaunchInstanceView, self).get_initial()
initial['project_id'] = self.request.user.project_id
initial['user_id'] = self.request.user.id
return initial
class DetailView(horizon_tabs.TabbedTableView):
tab_group_class = tabs.InstanceDetailTabs
template_name = 'project/databases/detail.html'
page_title = _("Instance Details: {{ instance.name }}")
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
instance = self.get_data()
table = tables.InstancesTable(self.request)
context["instance"] = instance
context["url"] = self.get_redirect_url()
context["actions"] = table.render_row_actions(instance)
return context
@memoized.memoized_method
def get_data(self):
try:
LOG.info("Obtaining instance for detailed view ")
instance_id = self.kwargs['instance_id']
instance = api.trove.instance_get(self.request, instance_id)
instance.host = tables.get_host(instance)
except Exception:
msg = _('Unable to retrieve details '
'for database instance: %s') % instance_id
exceptions.handle(self.request, msg,
redirect=self.get_redirect_url())
try:
instance.full_flavor = api.trove.flavor_get(
self.request, instance.flavor["id"])
except Exception:
LOG.error('Unable to retrieve flavor details'
' for database instance: %s' % instance_id)
return instance
def get_tabs(self, request, *args, **kwargs):
instance = self.get_data()
return self.tab_group_class(request, instance=instance, **kwargs)
@staticmethod
def get_redirect_url():
return reverse('horizon:project:databases:index')
class ResizeVolumeView(horizon_forms.ModalFormView):
form_class = forms.ResizeVolumeForm
template_name = 'project/databases/resize_volume.html'
success_url = reverse_lazy('horizon:project:databases:index')
page_title = _("Resize Database Volume")
@memoized.memoized_method
def get_object(self, *args, **kwargs):
instance_id = self.kwargs['instance_id']
try:
return api.trove.instance_get(self.request, instance_id)
except Exception:
msg = _('Unable to retrieve instance details.')
redirect = reverse('horizon:project:databases:index')
exceptions.handle(self.request, msg, redirect=redirect)
def get_context_data(self, **kwargs):
context = super(ResizeVolumeView, self).get_context_data(**kwargs)
context['instance_id'] = self.kwargs['instance_id']
return context
def get_initial(self):
instance = self.get_object()
return {'instance_id': self.kwargs['instance_id'],
'orig_size': instance.volume.get('size', 0)}
class ResizeInstanceView(horizon_forms.ModalFormView):
form_class = forms.ResizeInstanceForm
template_name = 'project/databases/resize_instance.html'
success_url = reverse_lazy('horizon:project:databases:index')
page_title = _("Resize Database Instance")
@memoized.memoized_method
def get_object(self, *args, **kwargs):
instance_id = self.kwargs['instance_id']
try:
instance = api.trove.instance_get(self.request, instance_id)
flavor_id = instance.flavor['id']
flavors = {}
for i, j in self.get_flavors():
flavors[str(i)] = j
if flavor_id in flavors:
instance.flavor_name = flavors[flavor_id]
else:
flavor = api.trove.flavor_get(self.request, flavor_id)
instance.flavor_name = flavor.name
return instance
except Exception:
redirect = reverse('horizon:project:databases:index')
msg = _('Unable to retrieve instance details.')
exceptions.handle(self.request, msg, redirect=redirect)
def get_context_data(self, **kwargs):
context = super(ResizeInstanceView, self).get_context_data(**kwargs)
context['instance_id'] = self.kwargs['instance_id']
return context
@memoized.memoized_method
def get_flavors(self, *args, **kwargs):
try:
flavors = api.trove.flavor_list(self.request)
return instance_utils.sort_flavor_list(self.request, flavors)
except Exception:
redirect = reverse("horizon:project:databases:index")
exceptions.handle(self.request,
_('Unable to retrieve flavors.'),
redirect=redirect)
def get_initial(self):
initial = super(ResizeInstanceView, self).get_initial()
obj = self.get_object()
if obj:
initial.update({'instance_id': self.kwargs['instance_id'],
'old_flavor_id': obj.flavor['id'],
'old_flavor_name': getattr(obj,
'flavor_name', ''),
'flavors': self.get_flavors()})
return initial

@ -0,0 +1,3 @@
# Importing non-modules that are not used explicitly
from .create_instance import LaunchInstance # noqa

@ -0,0 +1,420 @@
# Copyright 2013 Rackspace Hosting
#
# 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.conf import settings
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon.utils import memoized
from horizon import workflows
from openstack_dashboard import api as dash_api
from openstack_dashboard.contrib.trove import api
from openstack_dashboard.dashboards.project.instances \
import utils as instance_utils
LOG = logging.getLogger(__name__)
class SetInstanceDetailsAction(workflows.Action):
name = forms.CharField(max_length=80, label=_("Instance Name"))
flavor = forms.ChoiceField(label=_("Flavor"),
help_text=_("Size of image to launch."))
volume = forms.IntegerField(label=_("Volume Size"),
min_value=0,
initial=1,
help_text=_("Size of the volume in GB."))
datastore = forms.ChoiceField(label=_("Datastore"),
help_text=_(
"Type and version of datastore."))
class Meta(object):
name = _("Details")
help_text_template = "project/databases/_launch_details_help.html"
def clean(self):
if self.data.get("datastore", None) == "select_datastore_type_version":
msg = _("You must select a datastore type and version.")
self._errors["datastore"] = self.error_class([msg])
return self.cleaned_data
@memoized.memoized_method
def flavors(self, request):
try:
return api.trove.flavor_list(request)
except Exception:
LOG.exception("Exception while obtaining flavors list")
redirect = reverse("horizon:project:databases:index")
exceptions.handle(request,
_('Unable to obtain flavors.'),
redirect=redirect)
def populate_flavor_choices(self, request, context):
flavors = self.flavors(request)
if flavors:
return instance_utils.sort_flavor_list(request, flavors)
return []
@memoized.memoized_method
def datastores(self, request):
try:
return api.trove.datastore_list(request)
except Exception:
LOG.exception("Exception while obtaining datastores list")
self._datastores = []
@memoized.memoized_method
def datastore_versions(self, request, datastore):
try:
return api.trove.datastore_version_list(request, datastore)
except Exception:
LOG.exception("Exception while obtaining datastore version list")
self._datastore_versions = []
def populate_datastore_choices(self, request, context):
choices = ()
set_initial = False
datastores = self.datastores(request)
if datastores is not None:
num_datastores_with_one_version = 0
for ds in datastores:
versions = self.datastore_versions(request, ds.name)
if not set_initial:
if len(versions) >= 2:
set_initial = True
elif len(versions) == 1:
num_datastores_with_one_version += 1
if num_datastores_with_one_version > 1:
set_initial = True
if len(versions) > 0:
# only add to choices if datastore has at least one version
version_choices = ()
for v in versions:
version_choices = (version_choices +
((ds.name + ',' + v.name, v.name),))
datastore_choices = (ds.name, version_choices)
choices = choices + (datastore_choices,)
if set_initial:
# prepend choice to force user to choose
initial = (('select_datastore_type_version',
_('Select datastore type and version')))
choices = (initial,) + choices
return choices
TROVE_ADD_USER_PERMS = getattr(settings, 'TROVE_ADD_USER_PERMS', [])
TROVE_ADD_DATABASE_PERMS = getattr(settings, 'TROVE_ADD_DATABASE_PERMS', [])
TROVE_ADD_PERMS = TROVE_ADD_USER_PERMS + TROVE_ADD_DATABASE_PERMS
class SetInstanceDetails(workflows.Step):
action_class = SetInstanceDetailsAction
contributes = ("name", "volume", "flavor", "datastore")
class SetNetworkAction(workflows.Action):
network = forms.MultipleChoiceField(label=_("Networks"),
widget=forms.CheckboxSelectMultiple(),
error_messages={
'required': _(
"At least one network must"
" be specified.")},
help_text=_("Launch instance with"
" these networks"))
def __init__(self, request, *args, **kwargs):
super(SetNetworkAction, self).__init__(request, *args, **kwargs)
network_list = self.fields["network"].choices
if len(network_list) == 1:
self.fields['network'].initial = [network_list[0][0]]
class Meta(object):
name = _("Networking")
permissions = ('openstack.services.network',)
help_text = _("Select networks for your instance.")
def populate_network_choices(self, request, context):
try:
tenant_id = self.request.user.tenant_id
networks = dash_api.neutron.network_list_for_tenant(request,
tenant_id)
network_list = [(network.id, network.name_or_id)
for network in networks]
except Exception:
network_list = []
exceptions.handle(request,
_('Unable to retrieve networks.'))
return network_list
class SetNetwork(workflows.Step):
action_class = SetNetworkAction
template_name = "project/databases/_launch_networks.html"
contributes = ("network_id",)
def contribute(self, data, context):
if data:
networks = self.workflow.request.POST.getlist("network")
# If no networks are explicitly specified, network list
# contains an empty string, so remove it.
networks = [n for n in networks if n != '']
if networks:
context['network_id'] = networks
return context
class AddDatabasesAction(workflows.Action):
"""Initialize the database with users/databases. This tab will honor
the settings which should be a list of permissions required:
* TROVE_ADD_USER_PERMS = []
* TROVE_ADD_DATABASE_PERMS = []
"""
databases = forms.CharField(label=_('Initial Databases'),
required=False,
help_text=_('Comma separated list of '
'databases to create'))
user = forms.CharField(label=_('Initial Admin User'),
required=False,
help_text=_("Initial admin user to add"))
password = forms.CharField(widget=forms.PasswordInput(),
label=_("Password"),
required=False)
host = forms.CharField(label=_("Allowed Host (optional)"),
required=False,
help_text=_("Host or IP that the user is allowed "
"to connect through."))
class Meta(object):
name = _("Initialize Databases")
permissions = TROVE_ADD_PERMS
help_text_template = "project/databases/_launch_initialize_help.html"
def clean(self):
cleaned_data = super(AddDatabasesAction, self).clean()
if cleaned_data.get('user'):
if not cleaned_data.get('password'):
msg = _('You must specify a password if you create a user.')
self._errors["password"] = self.error_class([msg])
if not cleaned_data.get('databases'):
msg = _('You must specify at least one database if '
'you create a user.')
self._errors["databases"] = self.error_class([msg])
return cleaned_data
class InitializeDatabase(workflows.Step):
action_class = AddDatabasesAction
contributes = ["databases", 'user', 'password', 'host']
class AdvancedAction(workflows.Action):
initial_state = forms.ChoiceField(
label=_('Source for Initial State'),
required=False,
help_text=_("Choose initial state."),
choices=[
('', _('None')),
('backup', _('Restore from Backup')),
('master', _('Replicate from Instance'))],
widget=forms.Select(attrs={
'class': 'switchable',
'data-slug': 'initial_state'
}))
backup = forms.ChoiceField(
label=_('Backup Name'),
required=False,
help_text=_('Select a backup to restore'),
widget=forms.Select(attrs={
'class': 'switched',
'data-switch-on': 'initial_state',
'data-initial_state-backup': _('Backup Name')
}))
master = forms.ChoiceField(
label=_('Master Instance Name'),
required=False,
help_text=_('Select a master instance'),
widget=forms.Select(attrs={
'class': 'switched',
'data-switch-on': 'initial_state',
'data-initial_state-master': _('Master Instance Name')
}))
class Meta(object):
name = _("Advanced")
help_text_template = "project/databases/_launch_advanced_help.html"
def populate_backup_choices(self, request, context):
try:
backups = api.trove.backup_list(request)
choices = [(b.id, b.name) for b in backups
if b.status == 'COMPLETED']
except Exception:
choices = []
if choices:
choices.insert(0, ("", _("Select backup")))
else:
choices.insert(0, ("", _("No backups available")))
return choices
def populate_master_choices(self, request, context):
try:
instances = api.trove.instance_list(request)
choices = [(i.id, i.name) for i in
instances if i.status == 'ACTIVE']
except Exception:
choices = []
if choices:
choices.insert(0, ("", _("Select instance")))
else:
choices.insert(0, ("", _("No instances available")))
return choices
def clean(self):
cleaned_data = super(AdvancedAction, self).clean()
initial_state = cleaned_data.get("initial_state")
if initial_state == 'backup':
backup = self.cleaned_data['backup']
if backup:
try:
bkup = api.trove.backup_get(self.request, backup)
self.cleaned_data['backup'] = bkup.id
except Exception:
raise forms.ValidationError(_("Unable to find backup!"))
else:
raise forms.ValidationError(_("A backup must be selected!"))
cleaned_data['master'] = None
elif initial_state == 'master':
master = self.cleaned_data['master']
if master:
try:
api.trove.instance_get(self.request, master)
except Exception:
raise forms.ValidationError(
_("Unable to find master instance!"))
else:
raise forms.ValidationError(
_("A master instance must be selected!"))
cleaned_data['backup'] = None
else:
cleaned_data['master'] = None
cleaned_data['backup'] = None
return cleaned_data
class Advanced(workflows.Step):
action_class = AdvancedAction
contributes = ['backup', 'master']
class LaunchInstance(workflows.Workflow):
slug = "launch_instance"
name = _("Launch Instance")
finalize_button_name = _("Launch")
success_message = _('Launched %(count)s named "%(name)s".')
failure_message = _('Unable to launch %(count)s named "%(name)s".')
success_url = "horizon:project:databases:index"
default_steps = (SetInstanceDetails,
SetNetwork,
InitializeDatabase,
Advanced)
def __init__(self, request=None, context_seed=None, entry_point=None,
*args, **kwargs):
super(LaunchInstance, self).__init__(request, context_seed,
entry_point, *args, **kwargs)
self.attrs['autocomplete'] = (
settings.HORIZON_CONFIG.get('password_autocomplete'))
def format_status_message(self, message):
name = self.context.get('name', 'unknown instance')
return message % {"count": _("instance"), "name": name}
def _get_databases(self, context):
"""Returns the initial databases for this instance."""
databases = None
if context.get('databases'):
dbs = context['databases']
databases = [{'name': d.strip()} for d in dbs.split(',')]
return databases
def _get_users(self, context):
users = None
if context.get('user'):
user = {
'name': context['user'],
'password': context['password'],
'databases': self._get_databases(context),
}
if context['host']:
user['host'] = context['host']
users = [user]
return users
def _get_backup(self, context):
backup = None
if context.get('backup'):
backup = {'backupRef': context['backup']}
return backup
def _get_nics(self, context):
netids = context.get('network_id', None)
if netids:
return [{"net-id": netid, "v4-fixed-ip": ""}
for netid in netids]
else:
return None
def handle(self, request, context):
try:
datastore = self.context['datastore'].split(',')[0]
datastore_version = self.context['datastore'].split(',')[1]
LOG.info("Launching database instance with parameters "
"{name=%s, volume=%s, flavor=%s, "
"datastore=%s, datastore_version=%s, "
"dbs=%s, users=%s, "
"backups=%s, nics=%s, replica_of=%s}",
context['name'], context['volume'], context['flavor'],
datastore, datastore_version,
self._get_databases(context), self._get_users(context),
self._get_backup(context), self._get_nics(context),
context.get('master'))
api.trove.instance_create(request,
context['name'],
context['volume'],
context['flavor'],
datastore=datastore,
datastore_version=datastore_version,
databases=self._get_databases(context),
users=self._get_users(context),
restore_point=self._get_backup(context),
nics=self._get_nics(context),
replica_of=context.get('master'))
return True
except Exception:
exceptions.handle(request)
return False

@ -0,0 +1,38 @@
#
# 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 troveclient import client as trove_client
from openstack_dashboard.test import helpers
from openstack_dashboard.contrib.trove import api
class TroveAPITestCase(helpers.APITestCase):
def setUp(self):
super(TroveAPITestCase, self).setUp()
self._original_troveclient = api.trove.client
api.trove.client = lambda request: self.stub_troveclient()
def tearDown(self):
super(TroveAPITestCase, self).tearDown()
api.trove.client = self._original_troveclient
def stub_troveclient(self):
if not hasattr(self, "troveclient"):
self.mox.StubOutWithMock(trove_client, 'Client')
self.troveclient = self.mox.CreateMock(trove_client.Client)
return self.troveclient

@ -6,4 +6,5 @@ PANEL_DASHBOARD = 'project'
PANEL_GROUP = 'database'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.project.databases.panel.Databases'
ADD_PANEL = ('openstack_dashboard.contrib.trove.'
'content.databases.panel.Databases')

@ -6,5 +6,5 @@ PANEL_DASHBOARD = 'project'
PANEL_GROUP = 'database'
# Python panel class of the PANEL to be added.
ADD_PANEL = ('openstack_dashboard.dashboards.project.'
'database_backups.panel.Backups')
ADD_PANEL = ('openstack_dashboard.contrib.trove.'
'content.database_backups.panel.Backups')