Support of rescue instance in Horizon
Change-Id: Ie195befde8fe10ce419583ead06fdb759dd3813c Implements: blueprint instance-rescue-horizon-support
This commit is contained in:
parent
d45fb291d0
commit
ffa8b5404e
@ -722,6 +722,18 @@ def server_metadata_delete(request, instance_id, keys):
|
|||||||
novaclient(request).servers.delete_meta(instance_id, keys)
|
novaclient(request).servers.delete_meta(instance_id, keys)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace
|
||||||
|
def server_rescue(request, instance_id, password=None, image=None):
|
||||||
|
novaclient(request).servers.rescue(instance_id,
|
||||||
|
password=password,
|
||||||
|
image=image)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace
|
||||||
|
def server_unrescue(request, instance_id):
|
||||||
|
novaclient(request).servers.unrescue(instance_id)
|
||||||
|
|
||||||
|
|
||||||
@profiler.trace
|
@profiler.trace
|
||||||
def tenant_quota_get(request, tenant_id):
|
def tenant_quota_get(request, tenant_id):
|
||||||
return QuotaSet(novaclient(request).quotas.get(tenant_id))
|
return QuotaSet(novaclient(request).quotas.get(tenant_id))
|
||||||
|
@ -21,6 +21,8 @@ from horizon import forms
|
|||||||
from horizon import messages
|
from horizon import messages
|
||||||
|
|
||||||
from openstack_dashboard import api
|
from openstack_dashboard import api
|
||||||
|
from openstack_dashboard.dashboards.project.instances \
|
||||||
|
import forms as project_forms
|
||||||
|
|
||||||
|
|
||||||
class LiveMigrateForm(forms.SelfHandlingForm):
|
class LiveMigrateForm(forms.SelfHandlingForm):
|
||||||
@ -78,3 +80,7 @@ class LiveMigrateForm(forms.SelfHandlingForm):
|
|||||||
msg = _('Failed to live migrate instance to a new host.')
|
msg = _('Failed to live migrate instance to a new host.')
|
||||||
redirect = reverse('horizon:admin:instances:index')
|
redirect = reverse('horizon:admin:instances:index')
|
||||||
exceptions.handle(request, msg, redirect=redirect)
|
exceptions.handle(request, msg, redirect=redirect)
|
||||||
|
|
||||||
|
|
||||||
|
class RescueInstanceForm(project_forms.RescueInstanceForm):
|
||||||
|
failure_url = 'horizon:admin:instances:index'
|
||||||
|
@ -42,6 +42,10 @@ class AdminLogLink(project_tables.LogLink):
|
|||||||
url = "horizon:admin:instances:detail"
|
url = "horizon:admin:instances:detail"
|
||||||
|
|
||||||
|
|
||||||
|
class RescueInstance(project_tables.RescueInstance):
|
||||||
|
url = "horizon:admin:instances:rescue"
|
||||||
|
|
||||||
|
|
||||||
class MigrateInstance(policy.PolicyTargetMixin, tables.BatchAction):
|
class MigrateInstance(policy.PolicyTargetMixin, tables.BatchAction):
|
||||||
name = "migrate"
|
name = "migrate"
|
||||||
classes = ("btn-migrate",)
|
classes = ("btn-migrate",)
|
||||||
@ -190,7 +194,9 @@ class AdminInstancesTable(tables.DataTable):
|
|||||||
table_actions = (project_tables.DeleteInstance,
|
table_actions = (project_tables.DeleteInstance,
|
||||||
AdminInstanceFilterAction)
|
AdminInstanceFilterAction)
|
||||||
row_class = AdminUpdateRow
|
row_class = AdminUpdateRow
|
||||||
row_actions = (project_tables.ConfirmResize,
|
row_actions = (RescueInstance,
|
||||||
|
project_tables.UnRescueInstance,
|
||||||
|
project_tables.ConfirmResize,
|
||||||
project_tables.RevertResize,
|
project_tables.RevertResize,
|
||||||
AdminEditInstance,
|
AdminEditInstance,
|
||||||
AdminConsoleLink,
|
AdminConsoleLink,
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block form_id %}rescue_instance_form{% endblock %}
|
||||||
|
{% block form_action %}{% url "horizon:admin:instances:rescue" instance_id %}{% endblock %}
|
||||||
|
|
||||||
|
{% block modal_id %}rescue_instance_modal{% endblock %}
|
||||||
|
{% block modal-header %}{% trans "Rescue Instance" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-body %}
|
||||||
|
<div class="left">
|
||||||
|
<fieldset>
|
||||||
|
{% include "horizon/common/_form_fields.html" %}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% trans "The rescue mode is only for emergency purpose, for example in case of a system or access failure." %}</p>
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
This will shut down your instance and mount the root disk to a temporary server.
|
||||||
|
Then, you will be able to connect to this server, repair the system configuration or recover your data.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<p>{% trans "You may optionally select an image and set a password on the rescue instance server." %}</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Rescue Instance" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/instances/_rescue.html" %}
|
||||||
|
{% endblock %}
|
@ -35,4 +35,5 @@ urlpatterns = [
|
|||||||
url(INSTANCES % 'rdp', views.rdp, name='rdp'),
|
url(INSTANCES % 'rdp', views.rdp, name='rdp'),
|
||||||
url(INSTANCES % 'live_migrate', views.LiveMigrateView.as_view(),
|
url(INSTANCES % 'live_migrate', views.LiveMigrateView.as_view(),
|
||||||
name='live_migrate'),
|
name='live_migrate'),
|
||||||
|
url(INSTANCES % 'rescue', views.RescueView.as_view(), name='rescue'),
|
||||||
]
|
]
|
||||||
|
@ -247,3 +247,13 @@ class DetailView(views.DetailView):
|
|||||||
def _get_actions(self, instance):
|
def _get_actions(self, instance):
|
||||||
table = project_tables.AdminInstancesTable(self.request)
|
table = project_tables.AdminInstancesTable(self.request)
|
||||||
return table.render_row_actions(instance)
|
return table.render_row_actions(instance)
|
||||||
|
|
||||||
|
|
||||||
|
class RescueView(views.RescueView):
|
||||||
|
form_class = project_forms.RescueInstanceForm
|
||||||
|
submit_url = "horizon:admin:instances:rescue"
|
||||||
|
success_url = reverse_lazy('horizon:admin:instances:index')
|
||||||
|
template_name = 'admin/instances/rescue.html'
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
return {'instance_id': self.kwargs["instance_id"]}
|
||||||
|
@ -475,3 +475,39 @@ class Disassociate(forms.SelfHandlingForm):
|
|||||||
_('Unable to disassociate floating IP %s') % fip.ip,
|
_('Unable to disassociate floating IP %s') % fip.ip,
|
||||||
redirect=redirect)
|
redirect=redirect)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class RescueInstanceForm(forms.SelfHandlingForm):
|
||||||
|
image = forms.ChoiceField(
|
||||||
|
label=_("Select Image"),
|
||||||
|
widget=forms.ThemableSelectWidget(
|
||||||
|
attrs={'class': 'image-selector'},
|
||||||
|
data_attrs=('size', 'display-name'),
|
||||||
|
transform=_image_choice_title))
|
||||||
|
password = forms.CharField(label=_("Password"), max_length=255,
|
||||||
|
required=False,
|
||||||
|
widget=forms.PasswordInput(render_value=False))
|
||||||
|
failure_url = 'horizon:project:instances:index'
|
||||||
|
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
super(RescueInstanceForm, self).__init__(request, *args, **kwargs)
|
||||||
|
images = image_utils.get_available_images(request,
|
||||||
|
request.user.tenant_id)
|
||||||
|
choices = [(image.id, image) for image in images]
|
||||||
|
if not choices:
|
||||||
|
choices.insert(0, ("", _("No images available")))
|
||||||
|
self.fields['image'].choices = choices
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
try:
|
||||||
|
api.nova.server_rescue(request, self.initial["instance_id"],
|
||||||
|
password=data["password"],
|
||||||
|
image=data["image"])
|
||||||
|
messages.success(request,
|
||||||
|
_('Successfully rescued instance'))
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
redirect = reverse(self.failure_url)
|
||||||
|
exceptions.handle(request,
|
||||||
|
_('Unable to rescue instance'),
|
||||||
|
redirect=redirect)
|
||||||
|
@ -175,6 +175,49 @@ class SoftRebootInstance(RebootInstance):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class RescueInstance(policy.PolicyTargetMixin, tables.LinkAction):
|
||||||
|
name = "rescue"
|
||||||
|
verbose_name = _("Rescue Instance")
|
||||||
|
classes = ("btn-rescue", "ajax-modal")
|
||||||
|
url = "horizon:project:instances:rescue"
|
||||||
|
|
||||||
|
def get_link_url(self, datum):
|
||||||
|
instance_id = self.table.get_object_id(datum)
|
||||||
|
return urls.reverse(self.url, args=[instance_id])
|
||||||
|
|
||||||
|
def allowed(self, request, instance):
|
||||||
|
return instance.status in ACTIVE_STATES
|
||||||
|
|
||||||
|
|
||||||
|
class UnRescueInstance(tables.BatchAction):
|
||||||
|
name = 'unrescue'
|
||||||
|
classes = ("btn-unrescue",)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def action_present(count):
|
||||||
|
return ungettext_lazy(
|
||||||
|
u"Unrescue Instance",
|
||||||
|
u"Unrescue Instances",
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def action_past(count):
|
||||||
|
return ungettext_lazy(
|
||||||
|
u"Unrescued Instance",
|
||||||
|
u"Unrescued Instances",
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
def action(self, request, obj_id):
|
||||||
|
api.nova.server_unrescue(request, obj_id)
|
||||||
|
|
||||||
|
def allowed(self, request, instance=None):
|
||||||
|
if instance:
|
||||||
|
return instance.status == "RESCUE"
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class TogglePause(tables.BatchAction):
|
class TogglePause(tables.BatchAction):
|
||||||
name = "pause"
|
name = "pause"
|
||||||
icon = "pause"
|
icon = "pause"
|
||||||
@ -1263,6 +1306,7 @@ class InstancesTable(tables.DataTable):
|
|||||||
EditInstanceSecurityGroups,
|
EditInstanceSecurityGroups,
|
||||||
EditPortSecurityGroups,
|
EditPortSecurityGroups,
|
||||||
ConsoleLink, LogLink,
|
ConsoleLink, LogLink,
|
||||||
|
RescueInstance, UnRescueInstance,
|
||||||
TogglePause, ToggleSuspend, ToggleShelve,
|
TogglePause, ToggleSuspend, ToggleShelve,
|
||||||
ResizeLink, LockInstance, UnlockInstance,
|
ResizeLink, LockInstance, UnlockInstance,
|
||||||
SoftRebootInstance, RebootInstance,
|
SoftRebootInstance, RebootInstance,
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block form_id %}rescue_instance_form{% endblock %}
|
||||||
|
{% block form_action %}{% url "horizon:project:instances:rescue" instance_id %}{% endblock %}
|
||||||
|
|
||||||
|
{% block modal_id %}rescue_instance_modal{% endblock %}
|
||||||
|
{% block modal-header %}{% trans "Rescue Instance" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-body %}
|
||||||
|
<div class="left">
|
||||||
|
<fieldset>
|
||||||
|
{% include "horizon/common/_form_fields.html" %}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% trans "The rescue mode is only for emergency purpose, for example in case of a system or access failure." %}</p>
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
This will shut down your instance and mount the root disk to a temporary server.
|
||||||
|
Then, you will be able to connect to this server, repair the system configuration or recover your data.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<p>{% trans "You may optionally select an image and set a password on the rescue instance server." %}</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Rescue Instance" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "project/instances/_rescue.html" %}
|
||||||
|
{% endblock %}
|
@ -5023,6 +5023,66 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
|
|||||||
cleaned,
|
cleaned,
|
||||||
precleaned)
|
precleaned)
|
||||||
|
|
||||||
|
def _server_rescue_post(self, server_id, image_id,
|
||||||
|
password=None):
|
||||||
|
form_data = {'instance_id': server_id,
|
||||||
|
'image': image_id}
|
||||||
|
if password is not None:
|
||||||
|
form_data["password"] = password
|
||||||
|
url = reverse('horizon:project:instances:rescue',
|
||||||
|
args=[server_id])
|
||||||
|
return self.client.post(url, form_data)
|
||||||
|
|
||||||
|
@helpers.create_mocks({api.nova: ('server_rescue',),
|
||||||
|
api.glance: ('image_list_detailed',)})
|
||||||
|
def test_rescue_instance_post(self):
|
||||||
|
server = self.servers.first()
|
||||||
|
image = self.images.first()
|
||||||
|
password = u'testpass'
|
||||||
|
self._mock_glance_image_list_detailed(self.images.list())
|
||||||
|
self.mock_server_rescue.return_value = []
|
||||||
|
res = self._server_rescue_post(server.id, image.id,
|
||||||
|
password=password)
|
||||||
|
self.assertNoFormErrors(res)
|
||||||
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
self._check_glance_image_list_detailed(count=3)
|
||||||
|
self.mock_server_rescue.assert_called_once_with(
|
||||||
|
helpers.IsHttpRequest(), server.id, image=image.id,
|
||||||
|
password=password)
|
||||||
|
|
||||||
|
@helpers.create_mocks({api.nova: ('server_list',
|
||||||
|
'flavor_list',
|
||||||
|
'server_unrescue',),
|
||||||
|
api.glance: ('image_list_detailed',),
|
||||||
|
api.network: ('servers_update_addresses',)})
|
||||||
|
def test_unrescue_instance(self):
|
||||||
|
servers = self.servers.list()
|
||||||
|
server = servers[0]
|
||||||
|
server.status = "RESCUE"
|
||||||
|
|
||||||
|
self.mock_server_list.return_value = [servers, False]
|
||||||
|
self.mock_servers_update_addresses.return_value = None
|
||||||
|
self.mock_flavor_list.return_value = self.flavors.list()
|
||||||
|
self.mock_image_list_detailed.return_value = (self.images.list(),
|
||||||
|
False, False)
|
||||||
|
self.mock_server_unrescue.return_value = None
|
||||||
|
|
||||||
|
formData = {'action': 'instances__unrescue__%s' % server.id}
|
||||||
|
res = self.client.post(INDEX_URL, formData)
|
||||||
|
|
||||||
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
|
search_opts = {'marker': None, 'paginate': True}
|
||||||
|
self.mock_server_list.assert_called_once_with(helpers.IsHttpRequest(),
|
||||||
|
search_opts=search_opts)
|
||||||
|
self.mock_servers_update_addresses.assert_called_once_with(
|
||||||
|
helpers.IsHttpRequest(), servers)
|
||||||
|
self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest())
|
||||||
|
self.mock_image_list_detailed.assert_called_once_with(
|
||||||
|
helpers.IsHttpRequest())
|
||||||
|
self.mock_server_unrescue.assert_called_once_with(
|
||||||
|
helpers.IsHttpRequest(), server.id)
|
||||||
|
|
||||||
|
|
||||||
class InstanceAjaxTests(helpers.TestCase, InstanceTestHelperMixin):
|
class InstanceAjaxTests(helpers.TestCase, InstanceTestHelperMixin):
|
||||||
@helpers.create_mocks({api.nova: ("server_get",
|
@helpers.create_mocks({api.nova: ("server_get",
|
||||||
|
@ -57,4 +57,5 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
url(r'^(?P<instance_id>[^/]+)/ports/(?P<port_id>[^/]+)/update$',
|
url(r'^(?P<instance_id>[^/]+)/ports/(?P<port_id>[^/]+)/update$',
|
||||||
views.UpdatePortView.as_view(), name='update_port'),
|
views.UpdatePortView.as_view(), name='update_port'),
|
||||||
|
url(INSTANCES % 'rescue', views.RescueView.as_view(), name='rescue'),
|
||||||
]
|
]
|
||||||
|
@ -678,3 +678,22 @@ class UpdatePortView(port_views.UpdateView):
|
|||||||
initial = super(UpdatePortView, self).get_initial()
|
initial = super(UpdatePortView, self).get_initial()
|
||||||
initial['instance_id'] = self.kwargs['instance_id']
|
initial['instance_id'] = self.kwargs['instance_id']
|
||||||
return initial
|
return initial
|
||||||
|
|
||||||
|
|
||||||
|
class RescueView(forms.ModalFormView):
|
||||||
|
form_class = project_forms.RescueInstanceForm
|
||||||
|
template_name = 'project/instances/rescue.html'
|
||||||
|
submit_label = _("Confirm")
|
||||||
|
submit_url = "horizon:project:instances:rescue"
|
||||||
|
success_url = reverse_lazy('horizon:project:instances:index')
|
||||||
|
page_title = _("Rescue Instance")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(RescueView, self).get_context_data(**kwargs)
|
||||||
|
context["instance_id"] = self.kwargs['instance_id']
|
||||||
|
args = (self.kwargs['instance_id'],)
|
||||||
|
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
return {'instance_id': self.kwargs["instance_id"]}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
[:blueprint:`instance-rescue-horizon-support`]
|
||||||
|
Support instance rescue feature
|
Loading…
Reference in New Issue
Block a user