Merge "Support of rescue instance in Horizon"

This commit is contained in:
Zuul 2018-12-03 14:51:43 +00:00 committed by Gerrit Code Review
commit 119ce678da
15 changed files with 269 additions and 1 deletions

View File

@ -722,6 +722,18 @@ def server_metadata_delete(request, 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
def tenant_quota_get(request, tenant_id):
return QuotaSet(novaclient(request).quotas.get(tenant_id))

View File

@ -21,6 +21,8 @@ from horizon import forms
from horizon import messages
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.instances \
import forms as project_forms
class LiveMigrateForm(forms.SelfHandlingForm):
@ -78,3 +80,7 @@ class LiveMigrateForm(forms.SelfHandlingForm):
msg = _('Failed to live migrate instance to a new host.')
redirect = reverse('horizon:admin:instances:index')
exceptions.handle(request, msg, redirect=redirect)
class RescueInstanceForm(project_forms.RescueInstanceForm):
failure_url = 'horizon:admin:instances:index'

View File

@ -42,6 +42,10 @@ class AdminLogLink(project_tables.LogLink):
url = "horizon:admin:instances:detail"
class RescueInstance(project_tables.RescueInstance):
url = "horizon:admin:instances:rescue"
class MigrateInstance(policy.PolicyTargetMixin, tables.BatchAction):
name = "migrate"
classes = ("btn-migrate",)
@ -190,7 +194,9 @@ class AdminInstancesTable(tables.DataTable):
table_actions = (project_tables.DeleteInstance,
AdminInstanceFilterAction)
row_class = AdminUpdateRow
row_actions = (project_tables.ConfirmResize,
row_actions = (RescueInstance,
project_tables.UnRescueInstance,
project_tables.ConfirmResize,
project_tables.RevertResize,
AdminEditInstance,
AdminConsoleLink,

View File

@ -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 %}

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Rescue Instance" %}{% endblock %}
{% block main %}
{% include "admin/instances/_rescue.html" %}
{% endblock %}

View File

@ -35,4 +35,5 @@ urlpatterns = [
url(INSTANCES % 'rdp', views.rdp, name='rdp'),
url(INSTANCES % 'live_migrate', views.LiveMigrateView.as_view(),
name='live_migrate'),
url(INSTANCES % 'rescue', views.RescueView.as_view(), name='rescue'),
]

View File

@ -247,3 +247,13 @@ class DetailView(views.DetailView):
def _get_actions(self, instance):
table = project_tables.AdminInstancesTable(self.request)
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"]}

View File

@ -475,3 +475,39 @@ class Disassociate(forms.SelfHandlingForm):
_('Unable to disassociate floating IP %s') % fip.ip,
redirect=redirect)
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)

View File

@ -175,6 +175,49 @@ class SoftRebootInstance(RebootInstance):
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):
name = "pause"
icon = "pause"
@ -1263,6 +1306,7 @@ class InstancesTable(tables.DataTable):
EditInstanceSecurityGroups,
EditPortSecurityGroups,
ConsoleLink, LogLink,
RescueInstance, UnRescueInstance,
TogglePause, ToggleSuspend, ToggleShelve,
ResizeLink, LockInstance, UnlockInstance,
SoftRebootInstance, RebootInstance,

View File

@ -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 %}

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Rescue Instance" %}{% endblock %}
{% block main %}
{% include "project/instances/_rescue.html" %}
{% endblock %}

View File

@ -5023,6 +5023,66 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
cleaned,
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):
@helpers.create_mocks({api.nova: ("server_get",

View File

@ -57,4 +57,5 @@ urlpatterns = [
),
url(r'^(?P<instance_id>[^/]+)/ports/(?P<port_id>[^/]+)/update$',
views.UpdatePortView.as_view(), name='update_port'),
url(INSTANCES % 'rescue', views.RescueView.as_view(), name='rescue'),
]

View File

@ -678,3 +678,22 @@ class UpdatePortView(port_views.UpdateView):
initial = super(UpdatePortView, self).get_initial()
initial['instance_id'] = self.kwargs['instance_id']
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"]}

View File

@ -0,0 +1,5 @@
---
features:
- |
[:blueprint:`instance-rescue-horizon-support`]
Support instance rescue feature