From a5946f1cce1bd8b156acea5726c835f41da910ef Mon Sep 17 00:00:00 2001 From: liyingjun Date: Mon, 20 Apr 2015 11:59:54 +0800 Subject: [PATCH] Add support for attaching interface It would be nice to add attach interface support in horizon. Change-Id: Ibdf97247b1503d57606fc02c4e3a0e33e787d10e Closes-bug: #1445004 --- openstack_dashboard/api/nova.py | 8 ++++ .../dashboards/project/instances/forms.py | 34 ++++++++++++++++- .../dashboards/project/instances/tables.py | 20 +++++++++- .../instances/_attach_interface.html | 7 ++++ .../templates/instances/attach_interface.html | 7 ++++ .../dashboards/project/instances/tests.py | 38 +++++++++++++++++++ .../dashboards/project/instances/urls.py | 2 + .../dashboards/project/instances/views.py | 20 ++++++++++ 8 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 openstack_dashboard/dashboards/project/instances/templates/instances/_attach_interface.html create mode 100644 openstack_dashboard/dashboards/project/instances/templates/instances/attach_interface.html diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index c84c519dfb..8dd0b46394 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -917,6 +917,14 @@ def remove_host_from_aggregate(request, aggregate_id, host): return novaclient(request).aggregates.remove_host(aggregate_id, host) +def interface_attach(request, + server, port_id=None, net_id=None, fixed_ip=None): + return novaclient(request).servers.interface_attach(server, + port_id, + net_id, + fixed_ip) + + @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 6f0479dae3..3b6ecbfa11 100644 --- a/openstack_dashboard/dashboards/project/instances/forms.py +++ b/openstack_dashboard/dashboards/project/instances/forms.py @@ -24,7 +24,10 @@ from horizon import messages from horizon.utils import validators from openstack_dashboard import api -from openstack_dashboard.dashboards.project.images import utils +from openstack_dashboard.dashboards.project.images \ + import utils as image_utils +from openstack_dashboard.dashboards.project.instances \ + import utils as instance_utils def _image_choice_title(img): @@ -58,7 +61,8 @@ class RebuildInstanceForm(forms.SelfHandlingForm): instance_id = kwargs.get('initial', {}).get('instance_id') self.fields['instance_id'].initial = instance_id - images = utils.get_available_images(request, request.user.tenant_id) + images = image_utils.get_available_images(request, + request.user.tenant_id) choices = [(image.id, image) for image in images] if choices: choices.insert(0, ("", _("Select Image"))) @@ -162,3 +166,29 @@ class DecryptPasswordInstanceForm(forms.SelfHandlingForm): def handle(self, request, data): return True + + +class AttachInterface(forms.SelfHandlingForm): + instance_id = forms.CharField(widget=forms.HiddenInput()) + network = forms.ChoiceField(label=_("Network")) + + def __init__(self, request, *args, **kwargs): + super(AttachInterface, self).__init__(request, *args, **kwargs) + instance_id = kwargs.get('initial', {}).get('instance_id') + self.fields['instance_id'].initial = instance_id + networks = instance_utils.network_field_data(request, + include_empty_option=True) + self.fields['network'].choices = networks + + def handle(self, request, data): + instance = data.get('instance_id') + network = data.get('network') + try: + api.nova.interface_attach(request, instance, net_id=network) + msg = _('Attaching interface for instance %s.') % instance + messages.success(request, msg) + except Exception: + redirect = reverse('horizon:project:instances:index') + exceptions.handle(request, _("Unable to attach interface."), + redirect=redirect) + return True diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index e82b38bbf1..29fdacbd41 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -807,6 +807,24 @@ class UnlockInstance(policy.PolicyTargetMixin, tables.BatchAction): api.nova.server_unlock(request, obj_id) +class AttachInterface(policy.PolicyTargetMixin, tables.LinkAction): + name = "attach_interface" + verbose_name = _("Attach Interface") + classes = ("btn-confirm", "ajax-modal") + url = "horizon:project:instances:attach_interface" + policy_rules = (("compute", "compute_extension:attach_interfaces"),) + + 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 = {} @@ -1059,7 +1077,7 @@ class InstancesTable(tables.DataTable): InstancesFilterAction) row_actions = (StartInstance, ConfirmResize, RevertResize, CreateSnapshot, SimpleAssociateIP, AssociateIP, - SimpleDisassociateIP, EditInstance, + SimpleDisassociateIP, AttachInterface, EditInstance, DecryptInstancePassword, EditInstanceSecurityGroups, ConsoleLink, LogLink, TogglePause, ToggleSuspend, ResizeLink, LockInstance, UnlockInstance, diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_attach_interface.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_attach_interface.html new file mode 100644 index 0000000000..6aedfcff79 --- /dev/null +++ b/openstack_dashboard/dashboards/project/instances/templates/instances/_attach_interface.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Select the network for interface attaching." %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/attach_interface.html b/openstack_dashboard/dashboards/project/instances/templates/instances/attach_interface.html new file mode 100644 index 0000000000..040b1f24a0 --- /dev/null +++ b/openstack_dashboard/dashboards/project/instances/templates/instances/attach_interface.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% load i18n %} +{% block title %}{% trans "Attach Interface" %}{% endblock %} + +{% block main %} + {% include "project/instances/_attach_interface.html" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index ca9adc4bac..8397ca6567 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -4334,3 +4334,41 @@ class ConsoleManagerTests(helpers.TestCase): def test_invalid_console_type_raise_value_error(self): self.assertRaises(exceptions.NotAvailable, console.get_console, None, 'FAKE', None) + + @helpers.create_stubs({api.neutron: ('network_list_for_tenant',)}) + def test_interface_attach_get(self): + server = self.servers.first() + api.neutron.network_list_for_tenant(IsA(http.HttpRequest), + self.tenant.id) \ + .AndReturn(self.networks.list()[:1]) + + self.mox.ReplayAll() + + url = reverse('horizon:project:instances:attach_interface', + args=[server.id]) + res = self.client.get(url) + + self.assertTemplateUsed(res, + 'project/instances/attach_interface.html') + + @helpers.create_stubs({api.neutron: ('network_list_for_tenant',), + api.nova: ('interface_attach',)}) + def test_interface_attach_post(self): + server = self.servers.first() + network = api.neutron.network_list_for_tenant(IsA(http.HttpRequest), + self.tenant.id) \ + .AndReturn(self.networks.list()[:1]) + api.nova.interface_attach(IsA(http.HttpRequest), server.id, + net_id=network[0].id) + + self.mox.ReplayAll() + + form_data = {'instance_id': server.id, + 'network': network[0].id} + + url = reverse('horizon:project:instances:attach_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 71c0ddefd1..706dd40e54 100644 --- a/openstack_dashboard/dashboards/project/instances/urls.py +++ b/openstack_dashboard/dashboards/project/instances/urls.py @@ -44,4 +44,6 @@ urlpatterns = patterns( url(INSTANCES % 'resize', views.ResizeView.as_view(), name='resize'), url(INSTANCES_KEYPAIR % 'decryptpassword', views.DecryptPasswordView.as_view(), name='decryptpassword'), + url(INSTANCES % 'attach_interface', + views.AttachInterfaceView.as_view(), name='attach_interface'), ) diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py index 674e201ecf..0e0cfc1e91 100644 --- a/openstack_dashboard/dashboards/project/instances/views.py +++ b/openstack_dashboard/dashboards/project/instances/views.py @@ -425,3 +425,23 @@ class ResizeView(workflows.WorkflowView): 'old_flavor_name': getattr(_object, 'flavor_name', ''), 'flavors': self.get_flavors()}) return initial + + +class AttachInterfaceView(forms.ModalFormView): + form_class = project_forms.AttachInterface + template_name = 'project/instances/attach_interface.html' + modal_header = _("Attach Interface") + form_id = "attach_interface_form" + submit_label = _("Attach Interface") + submit_url = "horizon:project:instances:attach_interface" + success_url = reverse_lazy('horizon:project:instances:index') + + def get_context_data(self, **kwargs): + context = super(AttachInterfaceView, 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']}