Merge "Add missing replication functionality"
This commit is contained in:
commit
282cca6ee4
@ -121,7 +121,8 @@ def instance_delete(request, 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, volume_type=None):
|
||||
replica_of=None, replica_count=None,
|
||||
volume_type=None):
|
||||
# TODO(dklyle): adding conditional to support trove without volume
|
||||
# support for now until API supports checking for volume support
|
||||
if volume > 0:
|
||||
@ -140,7 +141,8 @@ def instance_create(request, name, volume, flavor, databases=None,
|
||||
nics=nics,
|
||||
datastore=datastore,
|
||||
datastore_version=datastore_version,
|
||||
replica_of=replica_of)
|
||||
replica_of=replica_of,
|
||||
replica_count=replica_count)
|
||||
|
||||
|
||||
def instance_resize_volume(request, instance_id, size):
|
||||
@ -165,6 +167,15 @@ def instance_detach_replica(request, instance_id):
|
||||
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):
|
||||
return troveclient(request).databases.list(instance_id)
|
||||
|
||||
|
@ -122,6 +122,26 @@ class ResizeInstanceForm(forms.SelfHandlingForm):
|
||||
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):
|
||||
instance_id = forms.CharField(widget=forms.HiddenInput())
|
||||
name = forms.CharField(label=_("Name"))
|
||||
|
@ -122,6 +122,49 @@ class DetachReplica(tables.BatchAction):
|
||||
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):
|
||||
@staticmethod
|
||||
def action_present(count):
|
||||
@ -594,9 +637,11 @@ class InstancesTable(tables.DataTable):
|
||||
row_actions = (CreateBackup,
|
||||
ResizeVolume,
|
||||
ResizeInstance,
|
||||
PromoteToReplicaSource,
|
||||
ManageRoot,
|
||||
RestartInstance,
|
||||
EjectReplicaSource,
|
||||
DetachReplica,
|
||||
RestartInstance,
|
||||
DeleteInstance)
|
||||
|
||||
|
||||
|
@ -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 %}
|
@ -0,0 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
{% include "project/databases/_promote_to_replica_source.html" %}
|
||||
{% endblock %}
|
@ -90,7 +90,7 @@ class DatabaseTests(test.TestCase):
|
||||
def test_index_pagination(self):
|
||||
# Mock database instances
|
||||
databases = self.databases.list()
|
||||
last_record = databases[1]
|
||||
last_record = databases[-1]
|
||||
databases = common.Paginated(databases, next_marker="foo")
|
||||
api.trove.instance_list(IsA(http.HttpRequest), marker=None)\
|
||||
.AndReturn(databases)
|
||||
@ -241,6 +241,7 @@ class DatabaseTests(test.TestCase):
|
||||
replica_of=None,
|
||||
users=None,
|
||||
nics=nics,
|
||||
replica_count=None,
|
||||
volume_type=None).AndReturn(self.databases.first())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
@ -307,6 +308,7 @@ class DatabaseTests(test.TestCase):
|
||||
replica_of=None,
|
||||
users=None,
|
||||
nics=nics,
|
||||
replica_count=None,
|
||||
volume_type=None).AndRaise(trove_exception)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
@ -999,6 +1001,7 @@ class DatabaseTests(test.TestCase):
|
||||
replica_of=self.databases.first().id,
|
||||
users=None,
|
||||
nics=nics,
|
||||
replica_count=2,
|
||||
volume_type=None).AndReturn(self.databases.first())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
@ -1010,8 +1013,107 @@ class DatabaseTests(test.TestCase):
|
||||
'datastore': 'mysql,5.5',
|
||||
'initial_state': 'master',
|
||||
'master': self.databases.first().id,
|
||||
'replica_count': 2,
|
||||
'volume_type': 'no_type'
|
||||
}
|
||||
|
||||
res = self.client.post(LAUNCH_URL, post)
|
||||
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)
|
||||
|
@ -39,6 +39,9 @@ urlpatterns = patterns(
|
||||
name='access_detail'),
|
||||
url(INSTANCES % 'create_database', views.CreateDatabaseView.as_view(),
|
||||
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(),
|
||||
name='manage_root'),
|
||||
)
|
||||
|
@ -357,6 +357,52 @@ class ResizeInstanceView(horizon_forms.ModalFormView):
|
||||
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):
|
||||
def __init__(self, instance_id, instance_name, enabled, password=None):
|
||||
self.id = instance_id
|
||||
|
@ -270,6 +270,17 @@ class AdvancedAction(workflows.Action):
|
||||
'data-switch-on': 'initial_state',
|
||||
'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):
|
||||
name = _("Advanced")
|
||||
@ -309,6 +320,7 @@ class AdvancedAction(workflows.Action):
|
||||
initial_state = cleaned_data.get("initial_state")
|
||||
|
||||
if initial_state == 'backup':
|
||||
cleaned_data['replica_count'] = None
|
||||
backup = self.cleaned_data['backup']
|
||||
if backup:
|
||||
try:
|
||||
@ -336,13 +348,14 @@ class AdvancedAction(workflows.Action):
|
||||
else:
|
||||
cleaned_data['master'] = None
|
||||
cleaned_data['backup'] = None
|
||||
cleaned_data['replica_count'] = None
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class Advanced(workflows.Step):
|
||||
action_class = AdvancedAction
|
||||
contributes = ['backup', 'master']
|
||||
contributes = ['backup', 'master', 'replica_count']
|
||||
|
||||
|
||||
class LaunchInstance(workflows.Workflow):
|
||||
@ -417,13 +430,13 @@ class LaunchInstance(workflows.Workflow):
|
||||
"{name=%s, volume=%s, volume_type=%s, flavor=%s, "
|
||||
"datastore=%s, datastore_version=%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'],
|
||||
self._get_volume_type(context), context['flavor'],
|
||||
datastore, datastore_version,
|
||||
self._get_databases(context), self._get_users(context),
|
||||
self._get_backup(context), self._get_nics(context),
|
||||
context.get('master'))
|
||||
context.get('master'), context['replica_count'])
|
||||
api.trove.instance_create(request,
|
||||
context['name'],
|
||||
context['volume'],
|
||||
@ -435,6 +448,7 @@ class LaunchInstance(workflows.Workflow):
|
||||
restore_point=self._get_backup(context),
|
||||
nics=self._get_nics(context),
|
||||
replica_of=context.get('master'),
|
||||
replica_count=context['replica_count'],
|
||||
volume_type=self._get_volume_type(
|
||||
context))
|
||||
return True
|
||||
|
@ -180,6 +180,39 @@ DATABASE_DATA_TWO = {
|
||||
"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 = {
|
||||
"instance_id": "6ddc36d9-73db-4e23-b52e-368937d72719",
|
||||
"status": "COMPLETED",
|
||||
@ -345,6 +378,8 @@ def data(TEST):
|
||||
DATABASE_DATA_ONE)
|
||||
database2 = instances.Instance(instances.Instances(None),
|
||||
DATABASE_DATA_TWO)
|
||||
database3 = instances.Instance(instances.Instances(None),
|
||||
DATABASE_DATA_THREE)
|
||||
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)
|
||||
@ -391,6 +426,7 @@ def data(TEST):
|
||||
|
||||
TEST.databases.add(database1)
|
||||
TEST.databases.add(database2)
|
||||
TEST.databases.add(database3)
|
||||
TEST.database_backups.add(bkup1)
|
||||
TEST.database_backups.add(bkup2)
|
||||
TEST.database_backups.add(bkup3)
|
||||
|
Loading…
Reference in New Issue
Block a user