Merge "Moving Trove to contrib"
This commit is contained in:
commit
cc75e4f0a8
openstack_dashboard
contrib/trove
api
content
database_backups
databases
__init__.pyforms.pypanel.pytables.pytabs.py
templates/databases
_detail_overview.html_detail_overview_cassandra.html_detail_overview_couchbase.html_detail_overview_mongodb.html_detail_overview_mysql.html_detail_overview_redis.html_launch_advanced_help.html_launch_details_help.html_launch_initialize_help.html_launch_network_help.html_launch_networks.html_resize_instance.html_resize_volume.htmlindex.htmllaunch.htmlresize_instance.htmlresize_volume.html
tests.pyurls.pyviews.pyworkflows
test
enabled
5
openstack_dashboard/contrib/trove/api/__init__.py
Normal file
5
openstack_dashboard/contrib/trove/api/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from openstack_dashboard.contrib.trove.api import trove
|
||||
|
||||
__all__ = [
|
||||
"trove"
|
||||
]
|
154
openstack_dashboard/contrib/trove/api/trove.py
Normal file
154
openstack_dashboard/contrib/trove/api/trove.py
Normal file
@ -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)
|
4
openstack_dashboard/contrib/trove/content/database_backups/templates/database_backups/_backup_details_help.html
Normal file
4
openstack_dashboard/contrib/trove/content/database_backups/templates/database_backups/_backup_details_help.html
Normal file
@ -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>
|
7
openstack_dashboard/contrib/trove/content/database_backups/templates/database_backups/backup.html
Normal file
7
openstack_dashboard/contrib/trove/content/database_backups/templates/database_backups/backup.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Backup Database" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'horizon/common/_workflow.html' %}
|
||||
{% endblock %}
|
74
openstack_dashboard/contrib/trove/content/database_backups/templates/database_backups/details.html
Normal file
74
openstack_dashboard/contrib/trove/content/database_backups/templates/database_backups/details.html
Normal file
@ -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 %}
|
7
openstack_dashboard/contrib/trove/content/database_backups/templates/database_backups/index.html
Normal file
7
openstack_dashboard/contrib/trove/content/database_backups/templates/database_backups/index.html
Normal file
@ -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
|
110
openstack_dashboard/contrib/trove/content/database_backups/workflows/create_backup.py
Normal file
110
openstack_dashboard/contrib/trove/content/database_backups/workflows/create_backup.py
Normal file
@ -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
|
93
openstack_dashboard/contrib/trove/content/databases/forms.py
Normal file
93
openstack_dashboard/contrib/trove/content/databases/forms.py
Normal file
@ -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
|
23
openstack_dashboard/contrib/trove/content/databases/panel.py
Normal file
23
openstack_dashboard/contrib/trove/content/databases/panel.py
Normal file
@ -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',)
|
410
openstack_dashboard/contrib/trove/content/databases/tables.py
Normal file
410
openstack_dashboard/contrib/trove/content/databases/tables.py
Normal file
@ -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)
|
127
openstack_dashboard/contrib/trove/content/databases/tabs.py
Normal file
127
openstack_dashboard/contrib/trove/content/databases/tabs.py
Normal file
@ -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
|
69
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview.html
Normal file
69
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview.html
Normal file
@ -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 %}
|
23
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_cassandra.html
Normal file
23
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_cassandra.html
Normal file
@ -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 %}
|
21
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_couchbase.html
Normal file
21
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_couchbase.html
Normal file
@ -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 %}
|
24
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_mongodb.html
Normal file
24
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_mongodb.html
Normal file
@ -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 %}
|
24
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_mysql.html
Normal file
24
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_mysql.html
Normal file
@ -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 %}
|
21
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_redis.html
Normal file
21
openstack_dashboard/contrib/trove/content/databases/templates/databases/_detail_overview_redis.html
Normal file
@ -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 %}
|
3
openstack_dashboard/contrib/trove/content/databases/templates/databases/_launch_advanced_help.html
Normal file
3
openstack_dashboard/contrib/trove/content/databases/templates/databases/_launch_advanced_help.html
Normal file
@ -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>
|
4
openstack_dashboard/contrib/trove/content/databases/templates/databases/_launch_details_help.html
Normal file
4
openstack_dashboard/contrib/trove/content/databases/templates/databases/_launch_details_help.html
Normal file
@ -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>
|
18
openstack_dashboard/contrib/trove/content/databases/templates/databases/_launch_initialize_help.html
Normal file
18
openstack_dashboard/contrib/trove/content/databases/templates/databases/_launch_initialize_help.html
Normal file
@ -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>
|
9
openstack_dashboard/contrib/trove/content/databases/templates/databases/_launch_network_help.html
Normal file
9
openstack_dashboard/contrib/trove/content/databases/templates/databases/_launch_network_help.html
Normal file
@ -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>
|
46
openstack_dashboard/contrib/trove/content/databases/templates/databases/_launch_networks.html
Normal file
46
openstack_dashboard/contrib/trove/content/databases/templates/databases/_launch_networks.html
Normal file
@ -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>
|
26
openstack_dashboard/contrib/trove/content/databases/templates/databases/_resize_instance.html
Normal file
26
openstack_dashboard/contrib/trove/content/databases/templates/databases/_resize_instance.html
Normal file
@ -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 %}
|
||||
|
27
openstack_dashboard/contrib/trove/content/databases/templates/databases/_resize_volume.html
Normal file
27
openstack_dashboard/contrib/trove/content/databases/templates/databases/_resize_volume.html
Normal file
@ -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 %}
|
7
openstack_dashboard/contrib/trove/content/databases/templates/databases/resize_instance.html
Normal file
7
openstack_dashboard/contrib/trove/content/databases/templates/databases/resize_instance.html
Normal file
@ -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 %}
|
7
openstack_dashboard/contrib/trove/content/databases/templates/databases/resize_volume.html
Normal file
7
openstack_dashboard/contrib/trove/content/databases/templates/databases/resize_volume.html
Normal file
@ -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 %}
|
543
openstack_dashboard/contrib/trove/content/databases/tests.py
Normal file
543
openstack_dashboard/contrib/trove/content/databases/tests.py
Normal file
@ -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)
|
33
openstack_dashboard/contrib/trove/content/databases/urls.py
Normal file
33
openstack_dashboard/contrib/trove/content/databases/urls.py
Normal file
@ -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')
|
||||
)
|
221
openstack_dashboard/contrib/trove/content/databases/views.py
Normal file
221
openstack_dashboard/contrib/trove/content/databases/views.py
Normal file
@ -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
openstack_dashboard/contrib/trove/test/__init__.py
Normal file
0
openstack_dashboard/contrib/trove/test/__init__.py
Normal file
38
openstack_dashboard/contrib/trove/test/helpers.py
Normal file
38
openstack_dashboard/contrib/trove/test/helpers.py
Normal file
@ -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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user