diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 8dd0b46394..bee4e65e4e 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -925,6 +925,10 @@ def interface_attach(request, fixed_ip) +def interface_detach(request, server, port_id): + return novaclient(request).servers.interface_detach(server, port_id) + + @memoized def list_extensions(request): return nova_list_extensions.ListExtManager(novaclient(request)).show_all() diff --git a/openstack_dashboard/dashboards/project/instances/forms.py b/openstack_dashboard/dashboards/project/instances/forms.py index 3b6ecbfa11..a0b4f72eb9 100644 --- a/openstack_dashboard/dashboards/project/instances/forms.py +++ b/openstack_dashboard/dashboards/project/instances/forms.py @@ -192,3 +192,44 @@ class AttachInterface(forms.SelfHandlingForm): exceptions.handle(request, _("Unable to attach interface."), redirect=redirect) return True + + +class DetachInterface(forms.SelfHandlingForm): + instance_id = forms.CharField(widget=forms.HiddenInput()) + port = forms.ChoiceField(label=_("Port")) + + def __init__(self, request, *args, **kwargs): + super(DetachInterface, self).__init__(request, *args, **kwargs) + instance_id = kwargs.get('initial', {}).get('instance_id') + self.fields['instance_id'].initial = instance_id + ports = [] + try: + ports = api.neutron.port_list(request, device_id=instance_id) + except Exception: + exceptions.handle(request, _('Unable to retrieve ports ' + 'information.')) + choices = [] + for port in ports: + ips = [] + for ip in port.fixed_ips: + ips.append(ip['ip_address']) + choices.append((port.id, ','.join(ips) or port.id)) + if choices: + choices.insert(0, ("", _("Select Port"))) + else: + choices.insert(0, ("", _("No Ports available"))) + self.fields['port'].choices = choices + + def handle(self, request, data): + instance = data.get('instance_id') + port = data.get('port') + try: + api.nova.interface_detach(request, instance, port) + msg = _('Detached interface %(port)s for instance ' + '%(instance)s.') % {'port': port, 'instance': instance} + messages.success(request, msg) + except Exception: + redirect = reverse('horizon:project:instances:index') + exceptions.handle(request, _("Unable to detach interface."), + redirect=redirect) + return True diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index 29fdacbd41..fe7c818c64 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -825,6 +825,25 @@ class AttachInterface(policy.PolicyTargetMixin, tables.LinkAction): return urlresolvers.reverse(self.url, args=[instance_id]) +# TODO(lyj): the policy for detach interface not exists in nova.json, +# once it's added, it should be added here. +class DetachInterface(policy.PolicyTargetMixin, tables.LinkAction): + name = "detach_interface" + verbose_name = _("Detach Interface") + classes = ("btn-confirm", "ajax-modal") + url = "horizon:project:instances:detach_interface" + + def allowed(self, request, instance): + return ((instance.status in ACTIVE_STATES + or instance.status == 'SHUTOFF') + and not is_deleting(instance) + and api.base.is_service_enabled(request, 'network')) + + def get_link_url(self, datum): + instance_id = self.table.get_object_id(datum) + return urlresolvers.reverse(self.url, args=[instance_id]) + + def get_ips(instance): template_name = 'project/instances/_instance_ips.html' ip_groups = {} @@ -1077,7 +1096,8 @@ class InstancesTable(tables.DataTable): InstancesFilterAction) row_actions = (StartInstance, ConfirmResize, RevertResize, CreateSnapshot, SimpleAssociateIP, AssociateIP, - SimpleDisassociateIP, AttachInterface, EditInstance, + SimpleDisassociateIP, AttachInterface, + DetachInterface, EditInstance, DecryptInstancePassword, EditInstanceSecurityGroups, ConsoleLink, LogLink, TogglePause, ToggleSuspend, ResizeLink, LockInstance, UnlockInstance, diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_detach_interface.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_detach_interface.html new file mode 100644 index 0000000000..d04efcad01 --- /dev/null +++ b/openstack_dashboard/dashboards/project/instances/templates/instances/_detach_interface.html @@ -0,0 +1,6 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Select the port to detach." %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/detach_interface.html b/openstack_dashboard/dashboards/project/instances/templates/instances/detach_interface.html new file mode 100644 index 0000000000..0267f38e91 --- /dev/null +++ b/openstack_dashboard/dashboards/project/instances/templates/instances/detach_interface.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% load i18n %} +{% block title %}{% trans "Detach Interface" %}{% endblock %} + +{% block main %} + {% include "project/instances/_detach_interface.html" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 8397ca6567..37dc3d0f67 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -4372,3 +4372,41 @@ class ConsoleManagerTests(helpers.TestCase): self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) + + @helpers.create_stubs({api.neutron: ('port_list',)}) + def test_interface_detach_get(self): + server = self.servers.first() + api.neutron.port_list(IsA(http.HttpRequest), + device_id=server.id)\ + .AndReturn([self.ports.first()]) + + self.mox.ReplayAll() + + url = reverse('horizon:project:instances:detach_interface', + args=[server.id]) + res = self.client.get(url) + + self.assertTemplateUsed(res, + 'project/instances/detach_interface.html') + + @helpers.create_stubs({api.neutron: ('port_list',), + api.nova: ('interface_detach',)}) + def test_interface_detach_post(self): + server = self.servers.first() + port = self.ports.first() + api.neutron.port_list(IsA(http.HttpRequest), + device_id=server.id)\ + .AndReturn([port]) + api.nova.interface_detach(IsA(http.HttpRequest), server.id, port.id) + + self.mox.ReplayAll() + + form_data = {'instance_id': server.id, + 'port': port.id} + + url = reverse('horizon:project:instances:detach_interface', + args=[server.id]) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/openstack_dashboard/dashboards/project/instances/urls.py b/openstack_dashboard/dashboards/project/instances/urls.py index 706dd40e54..e89c9a17ab 100644 --- a/openstack_dashboard/dashboards/project/instances/urls.py +++ b/openstack_dashboard/dashboards/project/instances/urls.py @@ -46,4 +46,6 @@ urlpatterns = patterns( views.DecryptPasswordView.as_view(), name='decryptpassword'), url(INSTANCES % 'attach_interface', views.AttachInterfaceView.as_view(), name='attach_interface'), + url(INSTANCES % 'detach_interface', + views.DetachInterfaceView.as_view(), name='detach_interface'), ) diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py index 0e0cfc1e91..1f6c1ca67d 100644 --- a/openstack_dashboard/dashboards/project/instances/views.py +++ b/openstack_dashboard/dashboards/project/instances/views.py @@ -445,3 +445,23 @@ class AttachInterfaceView(forms.ModalFormView): def get_initial(self): return {'instance_id': self.kwargs['instance_id']} + + +class DetachInterfaceView(forms.ModalFormView): + form_class = project_forms.DetachInterface + template_name = 'project/instances/detach_interface.html' + modal_header = _("Detach Interface") + form_id = "detach_interface_form" + submit_label = _("Detach Interface") + submit_url = "horizon:project:instances:detach_interface" + success_url = reverse_lazy('horizon:project:instances:index') + + def get_context_data(self, **kwargs): + context = super(DetachInterfaceView, 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']}