Merge "Add missing replication functionality"

This commit is contained in:
Jenkins 2016-04-01 16:31:24 +00:00 committed by Gerrit Code Review
commit 282cca6ee4
10 changed files with 323 additions and 7 deletions

View File

@ -121,7 +121,8 @@ def instance_delete(request, instance_id):
def instance_create(request, name, volume, flavor, databases=None, def instance_create(request, name, volume, flavor, databases=None,
users=None, restore_point=None, nics=None, users=None, restore_point=None, nics=None,
datastore=None, datastore_version=None, datastore=None, datastore_version=None,
replica_of=None, volume_type=None): replica_of=None, replica_count=None,
volume_type=None):
# TODO(dklyle): adding conditional to support trove without volume # TODO(dklyle): adding conditional to support trove without volume
# support for now until API supports checking for volume support # support for now until API supports checking for volume support
if volume > 0: if volume > 0:
@ -140,7 +141,8 @@ def instance_create(request, name, volume, flavor, databases=None,
nics=nics, nics=nics,
datastore=datastore, datastore=datastore,
datastore_version=datastore_version, datastore_version=datastore_version,
replica_of=replica_of) replica_of=replica_of,
replica_count=replica_count)
def instance_resize_volume(request, instance_id, size): def instance_resize_volume(request, instance_id, size):
@ -165,6 +167,15 @@ def instance_detach_replica(request, instance_id):
detach_replica_source=True) detach_replica_source=True)
def promote_to_replica_source(request, instance_id):
return troveclient(request).instances.promote_to_replica_source(
instance_id)
def eject_replica_source(request, instance_id):
return troveclient(request).instances.eject_replica_source(instance_id)
def database_list(request, instance_id): def database_list(request, instance_id):
return troveclient(request).databases.list(instance_id) return troveclient(request).databases.list(instance_id)

View File

@ -122,6 +122,26 @@ class ResizeInstanceForm(forms.SelfHandlingForm):
return True return True
class PromoteToReplicaSourceForm(forms.SelfHandlingForm):
instance_id = forms.CharField(widget=forms.HiddenInput())
def handle(self, request, data):
instance_id = data.get('instance_id')
name = self.initial['replica'].name
try:
api.trove.promote_to_replica_source(request, instance_id)
messages.success(
request,
_('Promoted replica "%s" as the new replica source.') % name)
except Exception as e:
redirect = reverse("horizon:project:databases:index")
exceptions.handle(
request,
_('Unable to promote replica as the new replica source. "%s"')
% e.message, redirect=redirect)
return True
class CreateUserForm(forms.SelfHandlingForm): class CreateUserForm(forms.SelfHandlingForm):
instance_id = forms.CharField(widget=forms.HiddenInput()) instance_id = forms.CharField(widget=forms.HiddenInput())
name = forms.CharField(label=_("Name")) name = forms.CharField(label=_("Name"))

View File

@ -122,6 +122,49 @@ class DetachReplica(tables.BatchAction):
api.trove.instance_detach_replica(request, obj_id) api.trove.instance_detach_replica(request, obj_id)
class PromoteToReplicaSource(tables.LinkAction):
name = "promote_to_replica_source"
verbose_name = _("Promote to Replica Source")
url = "horizon:project:databases:promote_to_replica_source"
classes = ("ajax-modal", "btn-promote-to-replica-source")
def allowed(self, request, instance=None):
return (instance.status in ACTIVE_STATES
and hasattr(instance, 'replica_of'))
def get_link_url(self, datum):
instance_id = self.table.get_object_id(datum)
return urlresolvers.reverse(self.url, args=[instance_id])
class EjectReplicaSource(tables.BatchAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Eject Replica Source",
u"Eject Replica Sources",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Ejected Replica Source",
u"Ejected Replica Sources",
count
)
name = "eject_replica_source"
classes = ('btn-danger', 'btn-eject-replica-source')
def _allowed(self, request, instance=None):
return (instance.status != 'PROMOTE'
and hasattr(instance, 'replicas'))
def action(self, request, obj_id):
api.trove.eject_replica_source(request, obj_id)
class GrantAccess(tables.BatchAction): class GrantAccess(tables.BatchAction):
@staticmethod @staticmethod
def action_present(count): def action_present(count):
@ -594,9 +637,11 @@ class InstancesTable(tables.DataTable):
row_actions = (CreateBackup, row_actions = (CreateBackup,
ResizeVolume, ResizeVolume,
ResizeInstance, ResizeInstance,
PromoteToReplicaSource,
ManageRoot, ManageRoot,
RestartInstance, EjectReplicaSource,
DetachReplica, DetachReplica,
RestartInstance,
DeleteInstance) DeleteInstance)

View File

@ -0,0 +1,34 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body %}
<div class="row">
<p class="col-md-12">{% trans 'Confirm the current replica is to be promoted as the new replica source.' %}</p>
<p class="col-md-12">{% trans 'This action cannot be undone.' %}</p>
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="row">
<div class="left">
<div class="well">
<h4>{% trans "Current Replica" %}</h4>
<table class='table table-bordered'>
<tr><th>{% trans 'Name' %}</th><td>{{ replica.name|default:_("None") }}</td></tr>
<tr><th>{% trans 'Host' %}</th><td>{{ replica.ip|default:_("Unknown") }}</td></tr>
<tr><th>{% trans 'Status' %}</th><td>{{ replica.status|default:_("Unknown") }}</td></tr>
</table>
</div>
</div>
<div class="right">
<div class="well">
<h4>{% trans "Current Replica Source" %}</h4>
<table class='table table-bordered'>
<tr><th>{% trans 'Name' %}</th><td>{{ replica_source.name|default:_("None") }}</td></tr>
<tr><th>{% trans 'Host' %}</th><td>{{ replica_source.ip|default:_("Unknown") }}</td></tr>
<tr><th>{% trans 'Status' %}</th><td>{{ replica_source.status|default:_("Unknown") }}</td></tr>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block main %}
{% include "project/databases/_promote_to_replica_source.html" %}
{% endblock %}

View File

@ -90,7 +90,7 @@ class DatabaseTests(test.TestCase):
def test_index_pagination(self): def test_index_pagination(self):
# Mock database instances # Mock database instances
databases = self.databases.list() databases = self.databases.list()
last_record = databases[1] last_record = databases[-1]
databases = common.Paginated(databases, next_marker="foo") databases = common.Paginated(databases, next_marker="foo")
api.trove.instance_list(IsA(http.HttpRequest), marker=None)\ api.trove.instance_list(IsA(http.HttpRequest), marker=None)\
.AndReturn(databases) .AndReturn(databases)
@ -241,6 +241,7 @@ class DatabaseTests(test.TestCase):
replica_of=None, replica_of=None,
users=None, users=None,
nics=nics, nics=nics,
replica_count=None,
volume_type=None).AndReturn(self.databases.first()) volume_type=None).AndReturn(self.databases.first())
self.mox.ReplayAll() self.mox.ReplayAll()
@ -307,6 +308,7 @@ class DatabaseTests(test.TestCase):
replica_of=None, replica_of=None,
users=None, users=None,
nics=nics, nics=nics,
replica_count=None,
volume_type=None).AndRaise(trove_exception) volume_type=None).AndRaise(trove_exception)
self.mox.ReplayAll() self.mox.ReplayAll()
@ -999,6 +1001,7 @@ class DatabaseTests(test.TestCase):
replica_of=self.databases.first().id, replica_of=self.databases.first().id,
users=None, users=None,
nics=nics, nics=nics,
replica_count=2,
volume_type=None).AndReturn(self.databases.first()) volume_type=None).AndReturn(self.databases.first())
self.mox.ReplayAll() self.mox.ReplayAll()
@ -1010,8 +1013,107 @@ class DatabaseTests(test.TestCase):
'datastore': 'mysql,5.5', 'datastore': 'mysql,5.5',
'initial_state': 'master', 'initial_state': 'master',
'master': self.databases.first().id, 'master': self.databases.first().id,
'replica_count': 2,
'volume_type': 'no_type' 'volume_type': 'no_type'
} }
res = self.client.post(LAUNCH_URL, post) res = self.client.post(LAUNCH_URL, post)
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({
api.trove: ('promote_to_replica_source',),
views.PromoteToReplicaSourceView: ('get_initial',)})
def test_promote_replica_instance(self):
replica_source = self.databases.first()
replica = self.databases.list()[1]
initial = {'instance_id': replica_source.id,
'replica': replica,
'replica_source': replica_source}
views.PromoteToReplicaSourceView.get_initial().AndReturn(initial)
api.trove.promote_to_replica_source(
IsA(http.HttpRequest), replica_source.id)
self.mox.ReplayAll()
url = reverse('horizon:project:databases:promote_to_replica_source',
args=[replica_source.id])
form = {
'instance_id': replica_source.id
}
res = self.client.post(url, form)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({
api.trove: ('promote_to_replica_source',),
views.PromoteToReplicaSourceView: ('get_initial',)})
def test_promote_replica_instance_exception(self):
replica_source = self.databases.first()
replica = self.databases.list()[1]
initial = {'instance_id': replica_source.id,
'replica': replica,
'replica_source': replica_source}
views.PromoteToReplicaSourceView.get_initial().AndReturn(initial)
api.trove.promote_to_replica_source(
IsA(http.HttpRequest), replica_source.id).\
AndRaise(self.exceptions.trove)
self.mox.ReplayAll()
url = reverse('horizon:project:databases:promote_to_replica_source',
args=[replica_source.id])
form = {
'instance_id': replica_source.id
}
res = self.client.post(url, form)
self.assertEqual(res.status_code, 302)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({
api.trove: ('instance_list',
'eject_replica_source',),
})
def test_eject_replica_source(self):
databases = common.Paginated(self.databases.list())
database = databases[2]
api.trove.eject_replica_source(
IsA(http.HttpRequest), database.id)
databases = common.Paginated(self.databases.list())
api.trove.instance_list(IsA(http.HttpRequest), marker=None)\
.AndReturn(databases)
self.mox.ReplayAll()
res = self.client.post(
INDEX_URL,
{'action': 'databases__eject_replica_source__%s' % database.id})
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({
api.trove: ('instance_list',
'eject_replica_source',),
})
def test_eject_replica_source_exception(self):
databases = common.Paginated(self.databases.list())
database = databases[2]
api.trove.eject_replica_source(
IsA(http.HttpRequest), database.id)\
.AndRaise(self.exceptions.trove)
databases = common.Paginated(self.databases.list())
api.trove.instance_list(IsA(http.HttpRequest), marker=None)\
.AndReturn(databases)
self.mox.ReplayAll()
res = self.client.post(
INDEX_URL,
{'action': 'databases__eject_replica_source__%s' % database.id})
self.assertRedirectsNoFollow(res, INDEX_URL)

View File

@ -39,6 +39,9 @@ urlpatterns = patterns(
name='access_detail'), name='access_detail'),
url(INSTANCES % 'create_database', views.CreateDatabaseView.as_view(), url(INSTANCES % 'create_database', views.CreateDatabaseView.as_view(),
name='create_database'), name='create_database'),
url(INSTANCES % 'promote_to_replica_source',
views.PromoteToReplicaSourceView.as_view(),
name='promote_to_replica_source'),
url(INSTANCES % 'manage_root', views.ManageRootView.as_view(), url(INSTANCES % 'manage_root', views.ManageRootView.as_view(),
name='manage_root'), name='manage_root'),
) )

View File

@ -357,6 +357,52 @@ class ResizeInstanceView(horizon_forms.ModalFormView):
return initial return initial
class PromoteToReplicaSourceView(horizon_forms.ModalFormView):
form_class = forms.PromoteToReplicaSourceForm
form_id = "promote_to_replica_source_form"
modal_header = _("Promote to Replica Source")
modal_id = "promote_to_replica_source_modal"
template_name = 'project/databases/promote_to_replica_source.html'
submit_lable = _("Promote")
submit_url = 'horizon:project:databases:promote_to_replica_source'
success_url = reverse_lazy('horizon:project:databases:index')
@memoized.memoized_method
def get_object(self, *args, **kwargs):
instance_id = self.kwargs['instance_id']
try:
replica = api.trove.instance_get(self.request, instance_id)
replica_source = api.trove.instance_get(self.request,
replica.replica_of['id'])
instances = {'replica': replica,
'replica_source': replica_source}
return instances
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(PromoteToReplicaSourceView, self).get_context_data(**kwargs)
context['instance_id'] = self.kwargs['instance_id']
context['replica'] = self.get_initial().get('replica')
context['replica'].ip = \
self.get_initial().get('replica').ip[0]
context['replica_source'] = self.get_initial().get('replica_source')
context['replica_source'].ip = \
self.get_initial().get('replica_source').ip[0]
args = (self.kwargs['instance_id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
def get_initial(self):
instances = self.get_object()
return {'instance_id': self.kwargs['instance_id'],
'replica': instances['replica'],
'replica_source': instances['replica_source']}
class EnableRootInfo(object): class EnableRootInfo(object):
def __init__(self, instance_id, instance_name, enabled, password=None): def __init__(self, instance_id, instance_name, enabled, password=None):
self.id = instance_id self.id = instance_id

View File

@ -270,6 +270,17 @@ class AdvancedAction(workflows.Action):
'data-switch-on': 'initial_state', 'data-switch-on': 'initial_state',
'data-initial_state-master': _('Master Instance Name') 'data-initial_state-master': _('Master Instance Name')
})) }))
replica_count = forms.IntegerField(
label=_('Replica Count'),
required=False,
min_value=1,
initial=1,
help_text=_('Specify the number of replicas to be created'),
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'initial_state',
'data-initial_state-master': _('Replica Count')
}))
class Meta(object): class Meta(object):
name = _("Advanced") name = _("Advanced")
@ -309,6 +320,7 @@ class AdvancedAction(workflows.Action):
initial_state = cleaned_data.get("initial_state") initial_state = cleaned_data.get("initial_state")
if initial_state == 'backup': if initial_state == 'backup':
cleaned_data['replica_count'] = None
backup = self.cleaned_data['backup'] backup = self.cleaned_data['backup']
if backup: if backup:
try: try:
@ -336,13 +348,14 @@ class AdvancedAction(workflows.Action):
else: else:
cleaned_data['master'] = None cleaned_data['master'] = None
cleaned_data['backup'] = None cleaned_data['backup'] = None
cleaned_data['replica_count'] = None
return cleaned_data return cleaned_data
class Advanced(workflows.Step): class Advanced(workflows.Step):
action_class = AdvancedAction action_class = AdvancedAction
contributes = ['backup', 'master'] contributes = ['backup', 'master', 'replica_count']
class LaunchInstance(workflows.Workflow): class LaunchInstance(workflows.Workflow):
@ -417,13 +430,13 @@ class LaunchInstance(workflows.Workflow):
"{name=%s, volume=%s, volume_type=%s, flavor=%s, " "{name=%s, volume=%s, volume_type=%s, flavor=%s, "
"datastore=%s, datastore_version=%s, " "datastore=%s, datastore_version=%s, "
"dbs=%s, users=%s, " "dbs=%s, users=%s, "
"backups=%s, nics=%s, replica_of=%s}", "backups=%s, nics=%s, replica_of=%s replica_count=%s}",
context['name'], context['volume'], context['name'], context['volume'],
self._get_volume_type(context), context['flavor'], self._get_volume_type(context), context['flavor'],
datastore, datastore_version, datastore, datastore_version,
self._get_databases(context), self._get_users(context), self._get_databases(context), self._get_users(context),
self._get_backup(context), self._get_nics(context), self._get_backup(context), self._get_nics(context),
context.get('master')) context.get('master'), context['replica_count'])
api.trove.instance_create(request, api.trove.instance_create(request,
context['name'], context['name'],
context['volume'], context['volume'],
@ -435,6 +448,7 @@ class LaunchInstance(workflows.Workflow):
restore_point=self._get_backup(context), restore_point=self._get_backup(context),
nics=self._get_nics(context), nics=self._get_nics(context),
replica_of=context.get('master'), replica_of=context.get('master'),
replica_count=context['replica_count'],
volume_type=self._get_volume_type( volume_type=self._get_volume_type(
context)) context))
return True return True

View File

@ -180,6 +180,39 @@ DATABASE_DATA_TWO = {
"id": "4d7b3f57-44f5-41d2-8e86-36b88cad572a", "id": "4d7b3f57-44f5-41d2-8e86-36b88cad572a",
} }
DATABASE_DATA_THREE = {
"status": "ACTIVE",
"updated": "2015-01-12T22:00:09",
"name": "Test Database with Config",
"links": [],
"created": "2015-01-12T22:00:03",
"ip": [
"10.0.0.3",
],
"volume": {
"used": 0.13,
"size": 1,
},
"flavor": {
"id": "1",
"links": [],
},
"datastore": {
"type": "mysql",
"version": "5.5"
},
"id": "c3369597-b53a-4bd4-bf54-41957c1291b8",
"configuration": {
"id": "0ef978d3-7c83-4192-ab86-b7a0a5010fa0",
"links": [],
"name": "config1"
},
"replicas": {
"id": "0ef978d3-7c83-4192-ab86-b7a0a5010fa0",
"links": [],
}
}
BACKUP_ONE = { BACKUP_ONE = {
"instance_id": "6ddc36d9-73db-4e23-b52e-368937d72719", "instance_id": "6ddc36d9-73db-4e23-b52e-368937d72719",
"status": "COMPLETED", "status": "COMPLETED",
@ -345,6 +378,8 @@ def data(TEST):
DATABASE_DATA_ONE) DATABASE_DATA_ONE)
database2 = instances.Instance(instances.Instances(None), database2 = instances.Instance(instances.Instances(None),
DATABASE_DATA_TWO) DATABASE_DATA_TWO)
database3 = instances.Instance(instances.Instances(None),
DATABASE_DATA_THREE)
bkup1 = backups.Backup(backups.Backups(None), BACKUP_ONE) bkup1 = backups.Backup(backups.Backups(None), BACKUP_ONE)
bkup2 = backups.Backup(backups.Backups(None), BACKUP_TWO) bkup2 = backups.Backup(backups.Backups(None), BACKUP_TWO)
bkup3 = backups.Backup(backups.Backups(None), BACKUP_TWO_INC) bkup3 = backups.Backup(backups.Backups(None), BACKUP_TWO_INC)
@ -391,6 +426,7 @@ def data(TEST):
TEST.databases.add(database1) TEST.databases.add(database1)
TEST.databases.add(database2) TEST.databases.add(database2)
TEST.databases.add(database3)
TEST.database_backups.add(bkup1) TEST.database_backups.add(bkup1)
TEST.database_backups.add(bkup2) TEST.database_backups.add(bkup2)
TEST.database_backups.add(bkup3) TEST.database_backups.add(bkup3)