Add support for trove incremental backups
Added extra column to backups tables (under instances and under backups) that will show Yes / No if the backup is incremental (ie it has a parent) When you drill down into backup details we now display the name of the backup parent if this is an incremental backup. This is displayed as a link with the backup name so it is navigable When you create a new backup you can now specify an existing backup to use as the parent Note: At some point we should probably have the create backup dialog filter the available parents based on the current selection for the instance. However, so far I haven't worked out how to make the backup parent choiceField change dynamically based on user updates to the instance choiceField Change-Id: I8bc70805aae50a019da3915f8e7aff25f6a7bbc6 Implements: blueprint trove-incremental-backup
This commit is contained in:
parent
96ee29c2e3
commit
e34fd6758e
|
@ -108,8 +108,9 @@ def backup_delete(request, backup_id):
|
|||
return troveclient(request).backups.delete(backup_id)
|
||||
|
||||
|
||||
def backup_create(request, name, instance_id, description=None):
|
||||
return troveclient(request).backups.create(name, instance_id, description)
|
||||
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):
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# under the License.
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template.defaultfilters import title # noqa
|
||||
from django.template import defaultfilters as d_filters
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
|
@ -123,6 +123,10 @@ def get_datastore_version(obj):
|
|||
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",
|
||||
|
@ -135,8 +139,13 @@ class BackupsTable(tables.DataTable):
|
|||
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",
|
||||
filters=(title, filters.replace_underscores),
|
||||
filters=(d_filters.title,
|
||||
filters.replace_underscores),
|
||||
verbose_name=_("Status"),
|
||||
status=True,
|
||||
status_choices=STATUS_CHOICES)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{% load i18n %}
|
||||
|
||||
<p>{% blocktrans %}Specify the details for the backup.{% endblocktrans %}</p>
|
||||
<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>
|
||||
|
|
|
@ -42,6 +42,20 @@
|
|||
</dl>
|
||||
</div>
|
||||
|
||||
{% if backup.parent %}
|
||||
<div class="status row-fluid detail">
|
||||
<h4>{% trans "Incremental Backup" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl>
|
||||
<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-fluid detail">
|
||||
<h4>{% trans "Database Info" %}</h4>
|
||||
|
|
|
@ -55,21 +55,45 @@ class DatabasesBackupsTests(test.TestCase):
|
|||
self.assertEqual(res.status_code, 200)
|
||||
self.assertMessageCount(res, error=1)
|
||||
|
||||
@test.create_stubs({api.trove: ('instance_list',)})
|
||||
@test.create_stubs({api.trove: ('instance_list',
|
||||
'backup_list',
|
||||
'backup_create')})
|
||||
def test_launch_backup(self):
|
||||
api.trove.instance_list(IsA(http.HttpRequest))\
|
||||
.AndReturn([])
|
||||
.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()
|
||||
|
||||
res = self.client.get(BACKUP_URL)
|
||||
self.assertTemplateUsed(res,
|
||||
'project/database_backups/backup.html')
|
||||
post = {
|
||||
'name': backupName,
|
||||
'instance': database.id,
|
||||
'description': backupDesc,
|
||||
'parent': ""
|
||||
}
|
||||
res = self.client.post(BACKUP_URL, post)
|
||||
|
||||
@test.create_stubs({api.trove: ('instance_list',)})
|
||||
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()
|
||||
|
||||
|
@ -78,6 +102,40 @@ class DatabasesBackupsTests(test.TestCase):
|
|||
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),
|
||||
|
@ -104,3 +162,21 @@ class DatabasesBackupsTests(test.TestCase):
|
|||
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')
|
||||
|
|
|
@ -87,6 +87,16 @@ class DetailView(horizon_views.APIView):
|
|||
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:
|
||||
|
|
|
@ -35,6 +35,9 @@ class BackupDetailsAction(workflows.Action):
|
|||
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:
|
||||
name = _("Details")
|
||||
|
@ -47,15 +50,31 @@ class BackupDetailsAction(workflows.Action):
|
|||
instances = api.trove.instance_list(request)
|
||||
except Exception:
|
||||
instances = []
|
||||
msg = _("Unable to list database instance to backup.")
|
||||
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"]
|
||||
contributes = ["name", "description", "instance", "parent"]
|
||||
|
||||
|
||||
class CreateBackup(workflows.Workflow):
|
||||
|
@ -81,7 +100,8 @@ class CreateBackup(workflows.Workflow):
|
|||
api.trove.backup_create(request,
|
||||
context['name'],
|
||||
context['instance'],
|
||||
context['description'])
|
||||
context['description'],
|
||||
context['parent'])
|
||||
return True
|
||||
except Exception:
|
||||
LOG.exception("Exception while creating backup")
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# under the License.
|
||||
|
||||
from django.core import urlresolvers
|
||||
from django.template.defaultfilters import title # noqa
|
||||
from django.template import defaultfilters as d_filters
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
|
@ -197,7 +197,8 @@ class InstancesTable(tables.DataTable):
|
|||
verbose_name=_("Volume Size"),
|
||||
attrs={'data-type': 'size'})
|
||||
status = tables.Column("status",
|
||||
filters=(title, filters.replace_underscores),
|
||||
filters=(d_filters.title,
|
||||
filters.replace_underscores),
|
||||
verbose_name=_("Status"),
|
||||
status=True,
|
||||
status_choices=STATUS_CHOICES)
|
||||
|
@ -242,6 +243,10 @@ class DatabaseTable(tables.DataTable):
|
|||
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"),
|
||||
|
@ -251,8 +256,13 @@ class InstanceBackupsTable(tables.DataTable):
|
|||
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",
|
||||
filters=(title, filters.replace_underscores),
|
||||
filters=(d_filters.title,
|
||||
filters.replace_underscores),
|
||||
verbose_name=_("Status"),
|
||||
status=True,
|
||||
status_choices=backup_tables.STATUS_CHOICES)
|
||||
|
|
|
@ -93,6 +93,19 @@ BACKUP_TWO = {
|
|||
}
|
||||
|
||||
|
||||
BACKUP_TWO_INC = {
|
||||
"instance_id": "4d7b3f57-44f5-41d2-8e86-36b88cad572a",
|
||||
"status": "COMPLETED",
|
||||
"updated": "2013-08-10T20:20:55",
|
||||
"locationRef": "http://swift/v1/AUTH/database_backups/f145.tar.gz",
|
||||
"name": "backup2-Incr",
|
||||
"created": "2013-08-10T20:20:37",
|
||||
"size": 0.13,
|
||||
"id": "e4602a3c-2bca-478f-b059-b6c215510fb5",
|
||||
"description": "Longer description of backup",
|
||||
"parent_id": "e4602a3c-2bca-478f-b059-b6c215510fb4",
|
||||
}
|
||||
|
||||
USER_ONE = {
|
||||
"name": "Test_User",
|
||||
"host": "%",
|
||||
|
@ -144,6 +157,7 @@ def data(TEST):
|
|||
DATABASE_DATA_TWO)
|
||||
bkup1 = backups.Backup(backups.Backups(None), BACKUP_ONE)
|
||||
bkup2 = backups.Backup(backups.Backups(None), BACKUP_TWO)
|
||||
bkup3 = backups.Backup(backups.Backups(None), BACKUP_TWO_INC)
|
||||
user1 = users.User(users.Users(None), USER_ONE)
|
||||
user_db1 = databases.Database(databases.Databases(None),
|
||||
USER_DB_ONE)
|
||||
|
@ -166,6 +180,7 @@ def data(TEST):
|
|||
TEST.databases.add(database2)
|
||||
TEST.database_backups.add(bkup1)
|
||||
TEST.database_backups.add(bkup2)
|
||||
TEST.database_backups.add(bkup3)
|
||||
TEST.database_users.add(user1)
|
||||
TEST.database_user_dbs.add(user_db1)
|
||||
TEST.datastores = utils.TestDataContainer()
|
||||
|
|
Loading…
Reference in New Issue