diff --git a/horizon/dashboards/nova/access_and_security/floating_ips/forms.py b/horizon/dashboards/nova/access_and_security/floating_ips/forms.py index 4810b6e53..dd74ce2bc 100644 --- a/horizon/dashboards/nova/access_and_security/floating_ips/forms.py +++ b/horizon/dashboards/nova/access_and_security/floating_ips/forms.py @@ -33,43 +33,6 @@ from horizon import forms LOG = logging.getLogger(__name__) -class FloatingIpAssociate(forms.SelfHandlingForm): - floating_ip_id = forms.CharField(widget=forms.HiddenInput()) - floating_ip = forms.CharField(label=_("Floating IP"), - widget=forms.TextInput( - attrs={'readonly': 'readonly'})) - instance_id = forms.ChoiceField(label=_("Instance ID")) - - def __init__(self, *args, **kwargs): - super(FloatingIpAssociate, self).__init__(*args, **kwargs) - instancelist = kwargs.get('initial', {}).get('instances', []) - if instancelist: - instancelist.insert(0, ("", _("Select an instance"))) - else: - instancelist = (("", _("No instances available")),) - self.fields['instance_id'] = forms.ChoiceField( - choices=instancelist, - label=_("Instance")) - - def handle(self, request, data): - ip_id = int(data['floating_ip_id']) - try: - api.server_add_floating_ip(request, - data['instance_id'], - ip_id) - LOG.info('Associating Floating IP "%s" with Instance "%s"' - % (data['floating_ip'], data['instance_id'])) - messages.success(request, - _('Successfully associated Floating IP %(ip)s ' - 'with Instance: %(inst)s') - % {"ip": data['floating_ip'], - "inst": data['instance_id']}) - except: - exceptions.handle(request, - _('Unable to associate floating IP.')) - return shortcuts.redirect('horizon:nova:access_and_security:index') - - class FloatingIpAllocate(forms.SelfHandlingForm): tenant_name = forms.CharField(widget=forms.HiddenInput()) pool = forms.ChoiceField(label=_("Pool")) diff --git a/horizon/dashboards/nova/access_and_security/floating_ips/tables.py b/horizon/dashboards/nova/access_and_security/floating_ips/tables.py index 231a16187..b1da2d205 100644 --- a/horizon/dashboards/nova/access_and_security/floating_ips/tables.py +++ b/horizon/dashboards/nova/access_and_security/floating_ips/tables.py @@ -20,6 +20,7 @@ import logging from django import shortcuts from django.contrib import messages from django.core import urlresolvers +from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ from horizon import api @@ -63,6 +64,11 @@ class AssociateIP(tables.LinkAction): return False return True + def get_link_url(self, datum): + base_url = urlresolvers.reverse(self.url) + params = urlencode({"ip_id": self.table.get_object_id(datum)}) + return "?".join([base_url, params]) + class DisassociateIP(tables.Action): name = "disassociate" @@ -94,7 +100,7 @@ def get_instance_info(instance): vals = {'INSTANCE_NAME': instance.instance_name, 'INSTANCE_ID': instance.instance_id} return info_string % vals - return _("Not available") + return None def get_instance_link(datum): diff --git a/horizon/dashboards/nova/access_and_security/floating_ips/tests.py b/horizon/dashboards/nova/access_and_security/floating_ips/tests.py index cee9bf90e..8bd686bf6 100644 --- a/horizon/dashboards/nova/access_and_security/floating_ips/tests.py +++ b/horizon/dashboards/nova/access_and_security/floating_ips/tests.py @@ -33,90 +33,90 @@ NAMESPACE = "horizon:nova:access_and_security:floating_ips" class FloatingIpViewTests(test.TestCase): def test_associate(self): - floating_ip = self.floating_ips.first() self.mox.StubOutWithMock(api.nova, 'server_list') - self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') + self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') api.nova.server_list(IsA(http.HttpRequest)) \ .AndReturn(self.servers.list()) - api.tenant_floating_ip_get(IsA(http.HttpRequest), - floating_ip.id).AndReturn(floating_ip) + api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) self.mox.ReplayAll() - url = reverse('%s:associate' % NAMESPACE, args=[floating_ip.id]) + url = reverse('%s:associate' % NAMESPACE) res = self.client.get(url) self.assertTemplateUsed(res, 'nova/access_and_security/floating_ips/associate.html') + workflow = res.context['workflow'] + choices = dict(workflow.steps[0].action.fields['ip_id'].choices) + # Verify that our "associated" floating IP isn't in the choices list. + self.assertTrue(self.floating_ips.get(id=1) not in choices) def test_associate_post(self): - floating_ip = self.floating_ips.first() + floating_ip = self.floating_ips.get(id=2) server = self.servers.first() - self.mox.StubOutWithMock(api, 'security_group_list') - self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') - self.mox.StubOutWithMock(api, 'server_add_floating_ip') - self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') + self.mox.StubOutWithMock(api.nova, 'server_add_floating_ip') + self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') self.mox.StubOutWithMock(api.nova, 'server_list') - self.mox.StubOutWithMock(api.nova, 'keypair_list') - api.nova.keypair_list(IsA(http.HttpRequest)) \ - .AndReturn(self.keypairs.list()) - api.security_group_list(IsA(http.HttpRequest)) \ - .AndReturn(self.security_groups.list()) + api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) api.nova.server_list(IsA(http.HttpRequest)) \ .AndReturn(self.servers.list()) - api.tenant_floating_ip_list(IsA(http.HttpRequest)) \ - .AndReturn(self.floating_ips.list()) - api.server_add_floating_ip(IsA(http.HttpRequest), - server.id, - floating_ip.id) - api.tenant_floating_ip_get(IsA(http.HttpRequest), - floating_ip.id).AndReturn(floating_ip) - api.nova.server_list(IsA(http.HttpRequest), - all_tenants=True).AndReturn(self.servers.list()) + api.nova.server_add_floating_ip(IsA(http.HttpRequest), + server.id, + floating_ip.id) self.mox.ReplayAll() form_data = {'instance_id': server.id, - 'floating_ip_id': floating_ip.id, - 'floating_ip': floating_ip.ip, - 'method': 'FloatingIpAssociate'} - url = reverse('%s:associate' % NAMESPACE, args=[floating_ip.id]) + 'ip_id': floating_ip.id} + url = reverse('%s:associate' % NAMESPACE) res = self.client.post(url, form_data) - self.assertRedirects(res, INDEX_URL) + self.assertRedirectsNoFollow(res, INDEX_URL) - def test_associate_post_with_exception(self): - floating_ip = self.floating_ips.first() + def test_associate_post_with_redirect(self): + floating_ip = self.floating_ips.get(id=2) server = self.servers.first() - self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') - self.mox.StubOutWithMock(api, 'security_group_list') - self.mox.StubOutWithMock(api.nova, 'keypair_list') - self.mox.StubOutWithMock(api, 'server_add_floating_ip') - self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') + self.mox.StubOutWithMock(api.nova, 'server_add_floating_ip') + self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') self.mox.StubOutWithMock(api.nova, 'server_list') + api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) api.nova.server_list(IsA(http.HttpRequest)) \ .AndReturn(self.servers.list()) - api.tenant_floating_ip_list(IsA(http.HttpRequest)) \ - .AndReturn(self.floating_ips.list()) - api.security_group_list(IsA(http.HttpRequest)) \ - .AndReturn(self.security_groups.list()) - api.nova.keypair_list(IsA(http.HttpRequest)) \ - .AndReturn(self.keypairs.list()) - api.server_add_floating_ip(IsA(http.HttpRequest), - server.id, - floating_ip.id) \ - .AndRaise(self.exceptions.nova) - api.tenant_floating_ip_get(IsA(http.HttpRequest), - floating_ip.id).AndReturn(floating_ip) - api.nova.server_list(IsA(http.HttpRequest), - all_tenants=True).AndReturn(self.servers.list()) + api.nova.server_add_floating_ip(IsA(http.HttpRequest), + server.id, + floating_ip.id) self.mox.ReplayAll() - url = reverse('%s:associate' % NAMESPACE, args=[floating_ip.id]) - res = self.client.post(url, - {'instance_id': 1, - 'floating_ip_id': floating_ip.id, - 'floating_ip': floating_ip.ip, - 'method': 'FloatingIpAssociate'}) - self.assertRedirects(res, INDEX_URL) + form_data = {'instance_id': server.id, + 'ip_id': floating_ip.id} + url = reverse('%s:associate' % NAMESPACE) + next = reverse("horizon:nova:instances_and_volumes:index") + res = self.client.post("%s?next=%s" % (url, next), form_data) + self.assertRedirectsNoFollow(res, next) + + def test_associate_post_with_exception(self): + floating_ip = self.floating_ips.get(id=2) + server = self.servers.first() + self.mox.StubOutWithMock(api.nova, 'server_add_floating_ip') + self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') + self.mox.StubOutWithMock(api.nova, 'server_list') + + api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) + api.nova.server_list(IsA(http.HttpRequest)) \ + .AndReturn(self.servers.list()) + api.nova.server_add_floating_ip(IsA(http.HttpRequest), + server.id, + floating_ip.id) \ + .AndRaise(self.exceptions.nova) + self.mox.ReplayAll() + + form_data = {'instance_id': server.id, + 'ip_id': floating_ip.id} + url = reverse('%s:associate' % NAMESPACE) + res = self.client.post(url, form_data) + self.assertRedirectsNoFollow(res, INDEX_URL) def test_disassociate_post(self): floating_ip = self.floating_ips.first() diff --git a/horizon/dashboards/nova/access_and_security/floating_ips/urls.py b/horizon/dashboards/nova/access_and_security/floating_ips/urls.py index 0dbd0a6c6..cc8da951a 100644 --- a/horizon/dashboards/nova/access_and_security/floating_ips/urls.py +++ b/horizon/dashboards/nova/access_and_security/floating_ips/urls.py @@ -24,10 +24,6 @@ from .views import AssociateView, AllocateView urlpatterns = patterns('', - url(r'^(?P[^/]+)/associate/$', - AssociateView.as_view(), - name='associate'), - url(r'^allocate/$', - AllocateView.as_view(), - name='allocate') + url(r'^associate/$', AssociateView.as_view(), name='associate'), + url(r'^allocate/$', AllocateView.as_view(), name='allocate') ) diff --git a/horizon/dashboards/nova/access_and_security/floating_ips/views.py b/horizon/dashboards/nova/access_and_security/floating_ips/views.py index 864230fa2..1a096b90b 100644 --- a/horizon/dashboards/nova/access_and_security/floating_ips/views.py +++ b/horizon/dashboards/nova/access_and_security/floating_ips/views.py @@ -22,60 +22,20 @@ """ Views for managing Nova floating IPs. """ -import logging -from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from horizon import api from horizon import exceptions from horizon import forms -from .forms import FloatingIpAssociate, FloatingIpAllocate +from horizon import workflows +from .forms import FloatingIpAllocate +from .workflows import IPAssociationWorkflow -LOG = logging.getLogger(__name__) - - -class AssociateView(forms.ModalFormView): - form_class = FloatingIpAssociate - template_name = 'nova/access_and_security/floating_ips/associate.html' - context_object_name = 'floating_ip' - - def get_object(self, *args, **kwargs): - ip_id = int(kwargs['ip_id']) - try: - return api.tenant_floating_ip_get(self.request, ip_id) - except: - redirect = reverse('horizon:nova:access_and_security:index') - exceptions.handle(self.request, - _('Unable to associate floating IP.'), - redirect=redirect) - - def get_initial(self): - try: - servers = api.nova.server_list(self.request) - except: - redirect = reverse('horizon:nova:access_and_security:index') - exceptions.handle(self.request, - _('Unable to retrieve instance list.'), - redirect=redirect) - instances = [] - for server in servers: - # FIXME(ttrifonov): show IP in case of non-unique names - # to be removed when nova can support unique names - server_name = server.name - if any(s.id != server.id and - s.name == server.name for s in servers): - # duplicate instance name - server_name = "%s [%s]" % (server.name, server.id) - instances.append((server.id, server_name)) - - # Sort instances for easy browsing - instances = sorted(instances, key=lambda x: x[1]) - - return {'floating_ip_id': self.object.id, - 'floating_ip': self.object.ip, - 'instances': instances} +class AssociateView(workflows.WorkflowView): + workflow_class = IPAssociationWorkflow + template_name = "nova/access_and_security/floating_ips/associate.html" class AllocateView(forms.ModalFormView): diff --git a/horizon/dashboards/nova/access_and_security/floating_ips/workflows.py b/horizon/dashboards/nova/access_and_security/floating_ips/workflows.py new file mode 100644 index 000000000..1c8f8bf09 --- /dev/null +++ b/horizon/dashboards/nova/access_and_security/floating_ips/workflows.py @@ -0,0 +1,116 @@ + +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Nebula, Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import forms +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import workflows + + +class AssociateIPAction(workflows.Action): + ip_id = forms.TypedChoiceField(label=_("IP Address"), + coerce=int, + empty_value=None) + instance_id = forms.ChoiceField(label=_("Instance")) + + class Meta: + name = _("IP Address") + help_text = _("Select the IP address you wish to associate with " + "the selected instance.") + + def populate_ip_id_choices(self, request, context): + try: + ips = api.nova.tenant_floating_ip_list(self.request) + except: + redirect = reverse('horizon:nova:access_and_security:index') + exceptions.handle(self.request, + _('Unable to retrieve floating IP addresses.'), + redirect=redirect) + options = sorted([(ip.id, ip.ip) for ip in ips if not ip.instance_id]) + if options: + options.insert(0, ("", _("Select an IP address"))) + else: + options = [("", _("No IP addresses available"))] + + return options + + def populate_instance_id_choices(self, request, context): + try: + servers = api.nova.server_list(self.request) + except: + redirect = reverse('horizon:nova:access_and_security:index') + exceptions.handle(self.request, + _('Unable to retrieve instance list.'), + redirect=redirect) + instances = [] + for server in servers: + # FIXME(ttrifonov): show IP in case of non-unique names + # to be removed when nova can support unique names + server_name = server.name + if any(s.id != server.id and + s.name == server.name for s in servers): + # duplicate instance name + server_name = "%s [%s]" % (server.name, server.id) + instances.append((server.id, server_name)) + + # Sort instances for easy browsing + instances = sorted(instances, key=lambda x: x[1]) + + if instances: + instances.insert(0, ("", _("Select an instance"))) + else: + instances = (("", _("No instances available")),) + return instances + + +class AssociateIP(workflows.Step): + action_class = AssociateIPAction + contributes = ("ip_id", "instance_id", "ip_address") + + def contribute(self, data, context): + context = super(AssociateIP, self).contribute(data, context) + ip_id = data.get('ip_id', None) + if ip_id: + ip_choices = dict(self.action.fields['ip_id'].choices) + context["ip_address"] = ip_choices.get(ip_id, None) + return context + + +class IPAssociationWorkflow(workflows.Workflow): + slug = "ip_association" + name = _("Manage Floating IP Associations") + finalize_button_name = _("Associate") + success_message = _('IP address %s associated.') + failure_message = _('Unable to associate IP address %s.') + success_url = "horizon:nova:access_and_security:index" + default_steps = (AssociateIP,) + + def format_status_message(self, message): + return message % self.context.get('ip_address', 'unknown IP address') + + def handle(self, request, data): + try: + api.nova.server_add_floating_ip(request, + data['instance_id'], + data['ip_id']) + except: + exceptions.handle(request) + return False + return True diff --git a/horizon/dashboards/nova/access_and_security/tests.py b/horizon/dashboards/nova/access_and_security/tests.py index eb2978ca0..a2e909e9a 100644 --- a/horizon/dashboards/nova/access_and_security/tests.py +++ b/horizon/dashboards/nova/access_and_security/tests.py @@ -57,7 +57,6 @@ class AccessAndSecurityTests(test.TestCase): floating_ips) def test_association(self): - floating_ip = self.floating_ips.first() servers = self.servers.list() # Add duplicate instance name to test instance name with [IP] @@ -68,23 +67,20 @@ class AccessAndSecurityTests(test.TestCase): server3.addresses['private'][0]['addr'] = "10.0.0.5" self.servers.add(server3) - self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') + self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') self.mox.StubOutWithMock(api.nova, 'server_list') - api.tenant_floating_ip_get(IsA(http.HttpRequest), - floating_ip.id).AndReturn(floating_ip) + api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) api.nova.server_list(IsA(http.HttpRequest)).AndReturn(servers) self.mox.ReplayAll() - res = self.client.get( - reverse("horizon:nova:access_and_security:" - "floating_ips:associate", - args=[floating_ip.id])) + res = self.client.get(reverse("horizon:nova:access_and_security:" + "floating_ips:associate")) self.assertTemplateUsed(res, - 'nova/access_and_security/' - 'floating_ips/associate.html') + 'nova/access_and_security/floating_ips/associate.html') - self.assertContains(res, '') - self.assertContains(res, '') + self.assertContains(res, + '') + self.assertContains(res, + '') self.assertContains(res, '') diff --git a/horizon/dashboards/nova/access_and_security/views.py b/horizon/dashboards/nova/access_and_security/views.py index 58c79fa57..92071b770 100644 --- a/horizon/dashboards/nova/access_and_security/views.py +++ b/horizon/dashboards/nova/access_and_security/views.py @@ -26,7 +26,6 @@ import logging from django.contrib import messages from django.utils.translation import ugettext_lazy as _ -from novaclient import exceptions as novaclient_exceptions from horizon import api from horizon import exceptions @@ -55,21 +54,19 @@ class IndexView(tables.MultiTableView): def get_security_groups_data(self): try: security_groups = api.security_group_list(self.request) - except novaclient_exceptions.ClientException, e: + except: security_groups = [] - LOG.exception("ClientException in security_groups index") - messages.error(self.request, - _('Error fetching security_groups: %s') % e) + exceptions.handle(self.request, + _('Unable to retrieve security groups.')) return security_groups def get_floating_ips_data(self): try: floating_ips = api.tenant_floating_ip_list(self.request) - except novaclient_exceptions.ClientException, e: + except: floating_ips = [] - LOG.exception("ClientException in floating ip index") - messages.error(self.request, - _('Error fetching floating ips: %s') % e) + exceptions.handle(self.request, + _('Unable to retrieve floating IP addresses.')) instances = [] try: diff --git a/horizon/dashboards/nova/instances_and_volumes/instances/tables.py b/horizon/dashboards/nova/instances_and_volumes/instances/tables.py index 3bb60d3b9..d4822da75 100644 --- a/horizon/dashboards/nova/instances_and_volumes/instances/tables.py +++ b/horizon/dashboards/nova/instances_and_volumes/instances/tables.py @@ -17,7 +17,9 @@ import logging from django import template +from django.core import urlresolvers from django.template.defaultfilters import title +from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ from horizon import api @@ -25,6 +27,8 @@ from horizon import tables from horizon.templatetags import sizeformat from horizon.utils.filters import replace_underscores +from horizon.dashboards.nova.access_and_security \ + .floating_ips.workflows import IPAssociationWorkflow from .tabs import InstanceDetailTabs, LogTab, VNCTab @@ -193,6 +197,21 @@ class LogLink(tables.LinkAction): return "?".join([base_url, tab_query_string]) +class AssociateIP(tables.LinkAction): + name = "associate" + verbose_name = _("Associate IP") + url = "horizon:nova:access_and_security:floating_ips:associate" + classes = ("ajax-modal", "btn-associate") + + def get_link_url(self, datum): + base_url = urlresolvers.reverse(self.url) + next = urlresolvers.reverse("horizon:nova:instances_and_volumes:index") + params = {"instance_id": self.table.get_object_id(datum), + IPAssociationWorkflow.redirect_param_name: next} + params = urlencode(params) + return "?".join([base_url, params]) + + class UpdateRow(tables.Row): ajax = True @@ -262,6 +281,6 @@ class InstancesTable(tables.DataTable): status_columns = ["status", "task"] row_class = UpdateRow table_actions = (LaunchLink, TerminateInstance) - row_actions = (SnapshotLink, EditInstance, ConsoleLink, LogLink, - TogglePause, ToggleSuspend, RebootInstance, + row_actions = (SnapshotLink, AssociateIP, EditInstance, ConsoleLink, + LogLink, TogglePause, ToggleSuspend, RebootInstance, TerminateInstance) diff --git a/horizon/dashboards/nova/instances_and_volumes/instances/workflows.py b/horizon/dashboards/nova/instances_and_volumes/instances/workflows.py index 1bb7e1991..1d59dcfb1 100644 --- a/horizon/dashboards/nova/instances_and_volumes/instances/workflows.py +++ b/horizon/dashboards/nova/instances_and_volumes/instances/workflows.py @@ -52,7 +52,7 @@ class SelectProjectUserAction(workflows.Action): class SelectProjectUser(workflows.Step): - action = SelectProjectUserAction + action_class = SelectProjectUserAction contributes = ("project_id", "user_id") @@ -135,7 +135,7 @@ class VolumeOptionsAction(workflows.Action): class VolumeOptions(workflows.Step): - action = VolumeOptionsAction + action_class = VolumeOptionsAction depends_on = ("project_id", "user_id") contributes = ("volume_type", "volume_id", @@ -278,7 +278,7 @@ class SetInstanceDetailsAction(workflows.Action): class SetInstanceDetails(workflows.Step): - action = SetInstanceDetailsAction + action_class = SetInstanceDetailsAction contributes = ("source_type", "source_id", "name", "count", "flavor") def contribute(self, data, context): @@ -338,7 +338,7 @@ class SetAccessControlsAction(workflows.Action): class SetAccessControls(workflows.Step): - action = SetAccessControlsAction + action_class = SetAccessControlsAction depends_on = ("project_id", "user_id") contributes = ("keypair_id", "security_group_ids") @@ -367,7 +367,7 @@ class CustomizeAction(workflows.Action): class PostCreationStep(workflows.Step): - action = CustomizeAction + action_class = CustomizeAction contributes = ("customization_script",) diff --git a/horizon/dashboards/nova/templates/nova/access_and_security/floating_ips/_associate.html b/horizon/dashboards/nova/templates/nova/access_and_security/floating_ips/_associate.html deleted file mode 100644 index 85283724d..000000000 --- a/horizon/dashboards/nova/templates/nova/access_and_security/floating_ips/_associate.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "horizon/common/_modal_form.html" %} -{% load i18n %} - -{% block form_id %}associate_floating_ip_form{% endblock %} -{% block form_action %}{% url horizon:nova:access_and_security:floating_ips:associate floating_ip.id %}{% endblock %} - -{% block modal-header %}{% trans "Associate Floating IP" %}{% endblock %} - -{% block modal-body %} -
-
- {% include "horizon/common/_form_fields.html" %} -
-
-
-

{% trans "Description:" %}

-

{% trans "Associate a floating ip with an instance." %}

-
-{% endblock %} - -{% block modal-footer %} - - {% trans "Cancel" %} -{% endblock %} diff --git a/horizon/dashboards/nova/templates/nova/access_and_security/floating_ips/associate.html b/horizon/dashboards/nova/templates/nova/access_and_security/floating_ips/associate.html index 737f27b50..7e6438baf 100644 --- a/horizon/dashboards/nova/templates/nova/access_and_security/floating_ips/associate.html +++ b/horizon/dashboards/nova/templates/nova/access_and_security/floating_ips/associate.html @@ -1,12 +1,11 @@ {% extends 'nova/base.html' %} {% load i18n %} -{% block title %}Associate Floating IPs{% endblock %} +{% block title %}{% trans "Associate Floating IP" %}{% endblock %} {% block page_header %} - {# to make searchable false, just remove it from the include statement #} {% include "horizon/common/_page_header.html" with title=_("Associate Floating IP") %} {% endblock page_header %} {% block dash_main %} - {% include 'nova/access_and_security/floating_ips/_associate.html' %} + {% include 'horizon/common/_workflow.html' %} {% endblock %} diff --git a/horizon/dashboards/syspanel/instances/tests.py b/horizon/dashboards/syspanel/instances/tests.py index 72a96718b..977025022 100644 --- a/horizon/dashboards/syspanel/instances/tests.py +++ b/horizon/dashboards/syspanel/instances/tests.py @@ -85,3 +85,69 @@ class InstanceViewTest(test.BaseAdminViewTests): self.assertContains(res, "512MB RAM | 1 VCPU | 0 Disk", 1, 200) self.assertContains(res, "Active", 1, 200) self.assertContains(res, "Running", 1, 200) + + def test_launch_post(self): + flavor = self.flavors.first() + image = self.images.first() + keypair = self.keypairs.first() + server = self.servers.first() + volume = self.volumes.first() + sec_group = self.security_groups.first() + customization_script = 'user data' + device_name = u'vda' + volume_choice = "%s:vol" % volume.id + block_device_mapping = {device_name: u"%s::0" % volume_choice} + + self.mox.StubOutWithMock(api.glance, 'image_list_detailed') + self.mox.StubOutWithMock(api.nova, 'flavor_list') + self.mox.StubOutWithMock(api.nova, 'keypair_list') + self.mox.StubOutWithMock(api.nova, 'security_group_list') + self.mox.StubOutWithMock(api.nova, 'volume_list') + self.mox.StubOutWithMock(api.nova, 'volume_snapshot_list') + self.mox.StubOutWithMock(api.nova, 'tenant_quota_usages') + self.mox.StubOutWithMock(api.nova, 'server_create') + + api.nova.flavor_list(IsA(http.HttpRequest)) \ + .AndReturn(self.flavors.list()) + api.nova.keypair_list(IsA(http.HttpRequest)) \ + .AndReturn(self.keypairs.list()) + api.nova.security_group_list(IsA(http.HttpRequest)) \ + .AndReturn(self.security_groups.list()) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'is_public': True}) \ + .AndReturn([self.images.list(), False]) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'property-owner_id': self.tenant.id}) \ + .AndReturn([[], False]) + api.nova.volume_list(IsA(http.HttpRequest)) \ + .AndReturn(self.volumes.list()) + api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) + api.nova.server_create(IsA(http.HttpRequest), + server.name, + image.id, + flavor.id, + keypair.name, + customization_script, + [sec_group.name], + block_device_mapping, + instance_count=IsA(int)) + self.mox.ReplayAll() + + form_data = {'flavor': flavor.id, + 'source_type': 'image_id', + 'image_id': image.id, + 'keypair': keypair.name, + 'name': server.name, + 'customization_script': customization_script, + 'project_id': self.tenants.first().id, + 'user_id': self.user.id, + 'groups': sec_group.name, + 'volume_type': 'volume_id', + 'volume_id': volume_choice, + 'device_name': device_name, + 'count': 1} + url = reverse('horizon:syspanel:instances:launch') + res = self.client.post(url, form_data) + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, + reverse('horizon:syspanel:instances:index')) diff --git a/horizon/templates/horizon/common/_workflow.html b/horizon/templates/horizon/common/_workflow.html index c37ec429f..41449f5dd 100644 --- a/horizon/templates/horizon/common/_workflow.html +++ b/horizon/templates/horizon/common/_workflow.html @@ -2,6 +2,7 @@ {% with workflow.get_entry_point as entry_point %}