Improved floating ip assocation via workflows.
Implements blueprint floating-ips-workflow. Change-Id: I2b850aa0f3e8f4e11d9bd94c97e1dc4336fa5bb1
This commit is contained in:
parent
8fd77f047f
commit
085e0728e4
@ -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"))
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -24,10 +24,6 @@ from .views import AssociateView, AllocateView
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^(?P<ip_id>[^/]+)/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')
|
||||
)
|
||||
|
@ -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):
|
||||
|
@ -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
|
@ -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, '<option value="1">server_1 [1]'
|
||||
'</option>')
|
||||
self.assertContains(res, '<option value="101">server_1 [101]'
|
||||
'</option>')
|
||||
self.assertContains(res,
|
||||
'<option value="1">server_1 [1]</option>')
|
||||
self.assertContains(res,
|
||||
'<option value="101">server_1 [101]</option>')
|
||||
self.assertContains(res, '<option value="2">server_2</option>')
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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",)
|
||||
|
||||
|
||||
|
@ -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 %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description:" %}</h3>
|
||||
<p>{% trans "Associate a floating ip with an instance." %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Associate IP" %}" />
|
||||
<a href="{% url horizon:nova:access_and_security:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -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 %}
|
||||
|
@ -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'))
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% with workflow.get_entry_point as entry_point %}
|
||||
<div class="workflow {% if modal %}modal hide{% else %}static_page{% endif %}">
|
||||
<form {{ workflow.attr_string|safe }} action="{{ workflow.get_absolute_url }}" method="POST">{% csrf_token %}
|
||||
{% if REDIRECT_URL %}<input type="hidden" name="{{ workflow.redirect_param_name }}" value="{{ REDIRECT_URL }}"/>{% endif %}
|
||||
<div class="modal-header">
|
||||
{% if modal %}<a href="#" class="close" data-dismiss="modal">×</a>{% endif %}
|
||||
<h3>{{ workflow.name }}</h3>
|
||||
|
@ -195,8 +195,8 @@ class ComputeApiTests(test.APITestCase):
|
||||
'used': 2,
|
||||
'flavor_fields': ['vcpus'],
|
||||
'quota': 10},
|
||||
'floating_ips': {'available': 0,
|
||||
'used': 1,
|
||||
'floating_ips': {'available': -1,
|
||||
'used': 2,
|
||||
'flavor_fields': [],
|
||||
'quota': 1}
|
||||
})
|
||||
|
@ -262,7 +262,12 @@ def data(TEST):
|
||||
'fixed_ip': '10.0.0.4',
|
||||
'instance_id': server_1.id,
|
||||
'ip': '58.58.58.58'})
|
||||
TEST.floating_ips.add(fip_1)
|
||||
fip_2 = floating_ips.FloatingIP(floating_ips.FloatingIPManager(None),
|
||||
{'id': 2,
|
||||
'fixed_ip': None,
|
||||
'instance_id': None,
|
||||
'ip': '58.58.58.58'})
|
||||
TEST.floating_ips.add(fip_1, fip_2)
|
||||
|
||||
# Usage
|
||||
usage_vals = {"tenant_id": TEST.tenant.id,
|
||||
|
@ -80,12 +80,12 @@ class AdminAction(workflows.Action):
|
||||
|
||||
|
||||
class TestStepOne(workflows.Step):
|
||||
action = TestActionOne
|
||||
action_class = TestActionOne
|
||||
contributes = ("project_id", "user_id")
|
||||
|
||||
|
||||
class TestStepTwo(workflows.Step):
|
||||
action = TestActionTwo
|
||||
action_class = TestActionTwo
|
||||
depends_on = ("project_id",)
|
||||
contributes = ("instance_id",)
|
||||
connections = {"project_id": (local_callback_func,
|
||||
@ -93,7 +93,7 @@ class TestStepTwo(workflows.Step):
|
||||
|
||||
|
||||
class TestExtraStep(workflows.Step):
|
||||
action = TestActionThree
|
||||
action_class = TestActionThree
|
||||
depends_on = ("project_id",)
|
||||
contributes = ("extra_data",)
|
||||
connections = {"project_id": (extra_callback_func,)}
|
||||
@ -102,7 +102,7 @@ class TestExtraStep(workflows.Step):
|
||||
|
||||
|
||||
class AdminStep(workflows.Step):
|
||||
action = AdminAction
|
||||
action_class = AdminAction
|
||||
contributes = ("admin_id",)
|
||||
after = TestStepOne
|
||||
before = TestStepTwo
|
||||
@ -188,10 +188,11 @@ class WorkflowsTests(test.TestCase):
|
||||
"user_id": self.user.id,
|
||||
"instance_id": self.servers.first().id}
|
||||
req = self.factory.post("/", seed)
|
||||
flow = TestWorkflow(req)
|
||||
flow = TestWorkflow(req, context_seed={"project_id": self.tenant.id})
|
||||
for step in flow.steps:
|
||||
if not step._action.is_valid():
|
||||
self.fail("Step %s was unexpectedly invalid." % step.slug)
|
||||
if not step.action.is_valid():
|
||||
self.fail("Step %s was unexpectedly invalid: %s"
|
||||
% (step.slug, step.action.errors))
|
||||
self.assertTrue(flow.is_valid())
|
||||
|
||||
# Additional items shouldn't affect validation
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
from django import template
|
||||
@ -32,6 +33,9 @@ from horizon.templatetags.horizon import can_haz
|
||||
from horizon.utils import html
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WorkflowContext(dict):
|
||||
def __init__(self, workflow, *args, **kwargs):
|
||||
super(WorkflowContext, self).__init__(*args, **kwargs)
|
||||
@ -156,7 +160,7 @@ class Action(forms.Form):
|
||||
context = template.RequestContext(self.request, extra_context)
|
||||
text += tmpl.render(context)
|
||||
else:
|
||||
text += linebreaks(self.help_text)
|
||||
text += linebreaks(force_unicode(self.help_text))
|
||||
return safe(text)
|
||||
|
||||
def handle(self, request, context):
|
||||
@ -254,7 +258,7 @@ class Step(object):
|
||||
|
||||
Inherited from the ``Action`` class.
|
||||
"""
|
||||
action = None
|
||||
action_class = None
|
||||
depends_on = ()
|
||||
contributes = ()
|
||||
connections = None
|
||||
@ -274,13 +278,12 @@ class Step(object):
|
||||
self.workflow = workflow
|
||||
|
||||
cls = self.__class__.__name__
|
||||
if not (self.action and issubclass(self.action, Action)):
|
||||
if not (self.action_class and issubclass(self.action_class, Action)):
|
||||
raise AttributeError("You must specify an action for %s." % cls)
|
||||
|
||||
self._action = None
|
||||
self.slug = self.action.slug
|
||||
self.name = self.action.name
|
||||
self.roles = self.action.roles
|
||||
self.slug = self.action_class.slug
|
||||
self.name = self.action_class.name
|
||||
self.roles = self.action_class.roles
|
||||
self.has_errors = False
|
||||
self._handlers = {}
|
||||
|
||||
@ -339,8 +342,16 @@ class Step(object):
|
||||
% (bits[-1], module_name, cls))
|
||||
self._handlers[key].append(handler)
|
||||
|
||||
def _init_action(self, request, data):
|
||||
self._action = self.action(request, data)
|
||||
@property
|
||||
def action(self):
|
||||
if not getattr(self, "_action", None):
|
||||
try:
|
||||
self._action = self.action_class(self.workflow.request,
|
||||
self.workflow.context)
|
||||
except:
|
||||
LOG.exception("Problem instantiating action class.")
|
||||
raise
|
||||
return self._action
|
||||
|
||||
def get_id(self):
|
||||
""" Returns the ID for this step. Suitable for use in HTML markup. """
|
||||
@ -350,7 +361,7 @@ class Step(object):
|
||||
for key in self.contributes:
|
||||
# Make sure we don't skip steps based on weird behavior of
|
||||
# POST query dicts.
|
||||
field = self._action.fields.get(key, None)
|
||||
field = self.action.fields.get(key, None)
|
||||
if field and field.required and not context.get(key):
|
||||
context.pop(key, None)
|
||||
failed_to_contribute = set(self.contributes)
|
||||
@ -381,15 +392,15 @@ class Step(object):
|
||||
def render(self):
|
||||
""" Renders the step. """
|
||||
step_template = template.loader.get_template(self.template_name)
|
||||
extra_context = {"form": self._action,
|
||||
extra_context = {"form": self.action,
|
||||
"step": self}
|
||||
context = template.RequestContext(self.workflow.request, extra_context)
|
||||
return step_template.render(context)
|
||||
|
||||
def get_help_text(self):
|
||||
""" Returns the help text for this step. """
|
||||
text = linebreaks(self.help_text)
|
||||
text += self._action.get_help_text()
|
||||
text = linebreaks(force_unicode(self.help_text))
|
||||
text += self.action.get_help_text()
|
||||
return safe(text)
|
||||
|
||||
|
||||
@ -470,6 +481,12 @@ class Workflow(html.HTMLElement):
|
||||
Path to the template which should be used to render this workflow.
|
||||
In general the default common template should be used. Default:
|
||||
``"horizon/common/_workflow.html"``.
|
||||
|
||||
.. attribute:: redirect_param_name
|
||||
|
||||
The name of a parameter used for tracking the URL to redirect to upon
|
||||
completion of the workflow. Defaults to ``"next"``.
|
||||
|
||||
"""
|
||||
__metaclass__ = WorkflowMetaclass
|
||||
slug = None
|
||||
@ -478,6 +495,7 @@ class Workflow(html.HTMLElement):
|
||||
finalize_button_name = _("Save")
|
||||
success_message = _("%s completed successfully.")
|
||||
failure_message = _("%s did not complete.")
|
||||
redirect_param_name = "next"
|
||||
_registerable_class = Step
|
||||
|
||||
def __unicode__(self):
|
||||
@ -517,11 +535,18 @@ class Workflow(html.HTMLElement):
|
||||
clean_seed = dict([(key, val)
|
||||
for key, val in context_seed.items()
|
||||
if key in self.contributions | self.depends_on])
|
||||
self.context_seed = clean_seed
|
||||
self.context.update(clean_seed)
|
||||
|
||||
for step in self.steps:
|
||||
self.context = step.contribute(request.POST, self.context)
|
||||
step._init_action(request, self.context)
|
||||
if request and request.method == "POST":
|
||||
for step in self.steps:
|
||||
valid = step.action.is_valid()
|
||||
# Be sure to use the CLEANED data if the workflow is valid.
|
||||
if valid:
|
||||
data = step.action.cleaned_data
|
||||
else:
|
||||
data = request.POST
|
||||
self.context = step.contribute(data, self.context)
|
||||
|
||||
@property
|
||||
def steps(self):
|
||||
@ -634,7 +659,7 @@ class Workflow(html.HTMLElement):
|
||||
# in one pass before returning.
|
||||
steps_valid = True
|
||||
for step in self.steps:
|
||||
if not step._action.is_valid():
|
||||
if not step.action.is_valid():
|
||||
steps_valid = False
|
||||
step.has_errors = True
|
||||
if not steps_valid:
|
||||
@ -651,7 +676,7 @@ class Workflow(html.HTMLElement):
|
||||
partial = False
|
||||
for step in self.steps:
|
||||
try:
|
||||
data = step._action.handle(self.request, self.context)
|
||||
data = step.action.handle(self.request, self.context)
|
||||
if data is True or data is None:
|
||||
continue
|
||||
elif data is False:
|
||||
|
@ -82,7 +82,10 @@ class WorkflowView(generic.TemplateView):
|
||||
context data to the template.
|
||||
"""
|
||||
context = super(WorkflowView, self).get_context_data(**kwargs)
|
||||
context[self.context_object_name] = self.get_workflow()
|
||||
workflow = self.get_workflow()
|
||||
context[self.context_object_name] = workflow
|
||||
next = self.request.REQUEST.get(workflow.redirect_param_name, None)
|
||||
context['REDIRECT_URL'] = next
|
||||
if self.request.is_ajax():
|
||||
context['modal'] = True
|
||||
return context
|
||||
@ -110,13 +113,13 @@ class WorkflowView(generic.TemplateView):
|
||||
except:
|
||||
success = False
|
||||
exceptions.handle(request)
|
||||
next = self.request.REQUEST.get(workflow.redirect_param_name, None)
|
||||
if success:
|
||||
msg = workflow.format_status_message(workflow.success_message)
|
||||
messages.success(request, msg)
|
||||
return shortcuts.redirect(workflow.get_success_url())
|
||||
else:
|
||||
msg = workflow.format_status_message(workflow.failure_message)
|
||||
messages.error(request, msg)
|
||||
return shortcuts.redirect(workflow.get_success_url())
|
||||
return shortcuts.redirect(next or workflow.get_success_url())
|
||||
else:
|
||||
return self.render_to_response(context)
|
||||
|
Loading…
Reference in New Issue
Block a user