Converts all of Access & Security to use new panels, modals, views, etc.
Adds empty table message, multi-table CBV, improved testing facilities. Fixes bug 905376. Fixes bug 905399. Change-Id: Ib93a5b9d09c9b98b0a6365f7d468efb05e28e676
This commit is contained in:
parent
120b43bd36
commit
22d80f7ff8
|
@ -61,3 +61,13 @@ Actions
|
|||
|
||||
.. autoclass:: FilterAction
|
||||
:members:
|
||||
|
||||
Class-Based Views
|
||||
=================
|
||||
|
||||
Several class-based views are provided to make working with DataTables
|
||||
easier in your UI.
|
||||
|
||||
.. autoclass:: DataTableView
|
||||
|
||||
.. autoclass:: MultiTableView
|
||||
|
|
|
@ -132,16 +132,9 @@ class SecurityGroup(APIResourceWrapper):
|
|||
_attrs = ['id', 'name', 'description', 'tenant_id', 'rules']
|
||||
|
||||
|
||||
class SecurityGroupRule(APIResourceWrapper):
|
||||
"""Simple wrapper around
|
||||
openstackx.extras.security_groups.SecurityGroupRule"""
|
||||
_attrs = ['id', 'parent_group_id', 'group_id', 'ip_protocol',
|
||||
'from_port', 'to_port', 'groups', 'ip_ranges']
|
||||
|
||||
|
||||
class SecurityGroupRule(APIResourceWrapper):
|
||||
"""Simple wrapper around openstackx.extras.users.User"""
|
||||
_attrs = ['id', 'name', 'description', 'tenant_id', 'security_group_rules']
|
||||
class SecurityGroupRule(APIDictWrapper):
|
||||
""" Simple wrapper for individual rules in a SecurityGroup. """
|
||||
_attrs = ['ip_protocol', 'from_port', 'to_port', 'ip_range']
|
||||
|
||||
|
||||
def novaclient(request):
|
||||
|
|
|
@ -33,22 +33,6 @@ from horizon import forms
|
|||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReleaseFloatingIp(forms.SelfHandlingForm):
|
||||
floating_ip_id = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
LOG.info('Releasing Floating IP "%s"' % data['floating_ip_id'])
|
||||
api.tenant_floating_ip_release(request, data['floating_ip_id'])
|
||||
messages.info(request, _('Successfully released Floating IP: %s')
|
||||
% data['floating_ip_id'])
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in ReleaseFloatingIp")
|
||||
messages.error(request, _('Error releasing Floating IP '
|
||||
'from tenant: %s') % e.message)
|
||||
return shortcuts.redirect(request.build_absolute_uri())
|
||||
|
||||
|
||||
class FloatingIpAssociate(forms.SelfHandlingForm):
|
||||
floating_ip_id = forms.CharField(widget=forms.HiddenInput())
|
||||
floating_ip = forms.CharField(widget=forms.TextInput(
|
||||
|
@ -69,58 +53,11 @@ class FloatingIpAssociate(forms.SelfHandlingForm):
|
|||
data['floating_ip_id'])
|
||||
LOG.info('Associating Floating IP "%s" with Instance "%s"'
|
||||
% (data['floating_ip'], data['instance_id']))
|
||||
messages.info(request, _('Successfully associated Floating IP: \
|
||||
messages.info(request, _('Successfully associated Floating IP \
|
||||
%(ip)s with Instance: %(inst)s'
|
||||
% {"ip": data['floating_ip'],
|
||||
"inst": data['instance_id']}))
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in FloatingIpAssociate")
|
||||
messages.error(request, _('Error associating Floating IP: %s')
|
||||
% e.message)
|
||||
return shortcuts.redirect(
|
||||
'horizon:nova:access_and_security:index')
|
||||
|
||||
|
||||
class FloatingIpDisassociate(forms.SelfHandlingForm):
|
||||
floating_ip_id = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
fip = api.tenant_floating_ip_get(request, data['floating_ip_id'])
|
||||
api.server_remove_floating_ip(request, fip.instance_id, fip.id)
|
||||
|
||||
LOG.info('Disassociating Floating IP "%s"'
|
||||
% data['floating_ip_id'])
|
||||
|
||||
messages.info(request,
|
||||
_('Successfully disassociated Floating IP: %s')
|
||||
% data['floating_ip_id'])
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in FloatingIpAssociate")
|
||||
messages.error(request, _('Error disassociating Floating IP: %s')
|
||||
% e.message)
|
||||
return shortcuts.redirect(
|
||||
'horizon:nova:access_and_security:index')
|
||||
|
||||
|
||||
class FloatingIpAllocate(forms.SelfHandlingForm):
|
||||
tenant_id = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
fip = api.tenant_floating_ip_allocate(request)
|
||||
LOG.info('Allocating Floating IP "%s" to tenant "%s"'
|
||||
% (fip.ip, data['tenant_id']))
|
||||
|
||||
messages.success(request,
|
||||
_('Successfully allocated Floating IP "%(ip)s"\
|
||||
to tenant "%(tenant)s"')
|
||||
% {"ip": fip.ip, "tenant": data['tenant_id']})
|
||||
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in FloatingIpAllocate")
|
||||
messages.error(request, _('Error allocating Floating IP "%(ip)s"\
|
||||
to tenant "%(tenant)s": %(msg)s') %
|
||||
{"ip": fip.ip, "tenant": data['tenant_id'], "msg": e.message})
|
||||
return shortcuts.redirect(
|
||||
'horizon:nova:access_and_security:index')
|
||||
messages.error(request, _('Error associating Floating IP: %s') % e)
|
||||
return shortcuts.redirect('horizon:nova:access_and_security:index')
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 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.
|
||||
|
||||
import logging
|
||||
|
||||
from django import shortcuts
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
|
||||
from horizon import api
|
||||
from horizon import tables
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AllocateIP(tables.Action):
|
||||
name = "allocate"
|
||||
verbose_name = _("Allocate IP To Tenant")
|
||||
requires_input = False
|
||||
|
||||
def single(self, data_table, request, *args):
|
||||
tenant_id = request.user.tenant_id
|
||||
try:
|
||||
fip = api.tenant_floating_ip_allocate(request)
|
||||
LOG.info('Allocating Floating IP "%s" to tenant "%s".'
|
||||
% (fip.ip, tenant_id))
|
||||
messages.success(request, _('Successfully allocated Floating IP '
|
||||
'"%(ip)s" to tenant "%(tenant)s".')
|
||||
% {"ip": fip.ip, "tenant": tenant_id})
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in FloatingIpAllocate")
|
||||
messages.error(request, _('Unable to allocate Floating IP '
|
||||
'"%(ip)s" to tenant "%(tenant)s".')
|
||||
% {"ip": fip.ip, "tenant": tenant_id})
|
||||
return shortcuts.redirect('horizon:nova:access_and_security:index')
|
||||
|
||||
|
||||
class ReleaseIP(tables.Action):
|
||||
name = "release"
|
||||
verbose_name = _("Release IP")
|
||||
classes = ('danger',)
|
||||
|
||||
def handle(self, table, request, object_ids):
|
||||
released = []
|
||||
for obj_id in object_ids:
|
||||
LOG.info('Releasing Floating IP "%s".' % obj_id)
|
||||
try:
|
||||
api.tenant_floating_ip_release(request, obj_id)
|
||||
# Floating IP ids are returned from the API as integers
|
||||
released.append(table.get_object_by_id(int(obj_id)))
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in ReleaseFloatingIp")
|
||||
messages.error(request, _('Unable to release Floating IP '
|
||||
'from tenant.'))
|
||||
if released:
|
||||
messages.info(request,
|
||||
_('Successfully released floating IPs: %s.')
|
||||
% ", ".join([ip.ip for ip in released]))
|
||||
return shortcuts.redirect('horizon:nova:access_and_security:index')
|
||||
|
||||
|
||||
class AssociateIP(tables.LinkAction):
|
||||
name = "associate"
|
||||
verbose_name = _("Associate IP")
|
||||
url = "horizon:nova:access_and_security:floating_ips:associate"
|
||||
attrs = {"class": "ajax-modal"}
|
||||
|
||||
def allowed(self, request, fip):
|
||||
if fip.instance_id:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class DisassociateIP(tables.Action):
|
||||
name = "disassociate"
|
||||
verbose_name = _("Disassociate IP")
|
||||
|
||||
def allowed(self, request, fip):
|
||||
if fip.instance_id:
|
||||
return True
|
||||
return False
|
||||
|
||||
def single(self, table, request, obj_id):
|
||||
try:
|
||||
fip = table.get_object_by_id(int(obj_id))
|
||||
api.server_remove_floating_ip(request, fip.instance_id, fip.id)
|
||||
LOG.info('Disassociating Floating IP "%s".' % obj_id)
|
||||
messages.info(request,
|
||||
_('Successfully disassociated Floating IP: %s')
|
||||
% obj_id)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in FloatingIpAssociate")
|
||||
messages.error(request, _('Error disassociating Floating IP: %s')
|
||||
% e.message)
|
||||
return shortcuts.redirect('horizon:nova:access_and_security:index')
|
||||
|
||||
|
||||
class FloatingIPsTable(tables.DataTable):
|
||||
ip = tables.Column("ip", verbose_name=_("IP Address"))
|
||||
instance = tables.Column("instance_id",
|
||||
verbose_name=_("Instance"),
|
||||
empty_value="-")
|
||||
|
||||
class Meta:
|
||||
name = "floating_ips"
|
||||
verbose_name = _("Floating IPs")
|
||||
table_actions = (AllocateIP, ReleaseIP)
|
||||
row_actions = (AssociateIP, DisassociateIP, ReleaseIP)
|
|
@ -21,7 +21,6 @@
|
|||
import datetime
|
||||
|
||||
from django import http
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.shortcuts import redirect
|
||||
from mox import IsA, IgnoreArg
|
||||
|
@ -33,7 +32,7 @@ from horizon.dashboards.nova.access_and_security.floating_ips.forms import \
|
|||
FloatingIpAssociate
|
||||
|
||||
|
||||
FLOATING_IPS_INDEX = reverse('horizon:nova:access_and_security:index')
|
||||
INDEX_URL = reverse('horizon:nova:access_and_security:index')
|
||||
|
||||
|
||||
class FloatingIpViewTests(test.BaseViewTests):
|
||||
|
@ -111,7 +110,7 @@ class FloatingIpViewTests(test.BaseViewTests):
|
|||
'floating_ip': self.floating_ip.ip,
|
||||
'method': 'FloatingIpAssociate'})
|
||||
|
||||
self.assertRedirects(res, FLOATING_IPS_INDEX)
|
||||
self.assertRedirects(res, INDEX_URL)
|
||||
|
||||
def test_associate_post_with_exception(self):
|
||||
server = self.server
|
||||
|
@ -152,70 +151,51 @@ class FloatingIpViewTests(test.BaseViewTests):
|
|||
'method': 'FloatingIpAssociate'})
|
||||
self.assertRaises(novaclient_exceptions.ClientException)
|
||||
|
||||
self.assertRedirects(res, FLOATING_IPS_INDEX)
|
||||
|
||||
def test_disassociate(self):
|
||||
res = self.client.get(
|
||||
reverse('horizon:nova:access_and_security:floating_ips:disassociate',
|
||||
args=[1]))
|
||||
self.assertTemplateUsed(res,
|
||||
'nova/access_and_security/floating_ips/associate.html')
|
||||
self.assertRedirects(res, INDEX_URL)
|
||||
|
||||
def test_disassociate_post(self):
|
||||
self.mox.StubOutWithMock(api, 'tenant_floating_ip_list')
|
||||
api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.floating_ips)
|
||||
self.mox.StubOutWithMock(api.nova, 'keypair_list')
|
||||
self.mox.StubOutWithMock(api, 'security_group_list')
|
||||
self.mox.StubOutWithMock(api, 'tenant_floating_ip_list')
|
||||
self.mox.StubOutWithMock(api, 'tenant_floating_ip_get')
|
||||
self.mox.StubOutWithMock(api, 'server_remove_floating_ip')
|
||||
|
||||
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||
api.security_group_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.security_groups)
|
||||
self.mox.StubOutWithMock(api.nova, 'keypair_list')
|
||||
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||
api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.floating_ips)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'server_remove_floating_ip')
|
||||
api.server_remove_floating_ip = self.mox.CreateMockAnything()
|
||||
api.server_remove_floating_ip(IsA(http.HttpRequest), IsA(int),
|
||||
IsA(int)).\
|
||||
AndReturn(None)
|
||||
self.mox.StubOutWithMock(messages, 'info')
|
||||
messages.info(IsA(http.HttpRequest), IsA(unicode))
|
||||
|
||||
self.mox.StubOutWithMock(api, 'tenant_floating_ip_get')
|
||||
api.tenant_floating_ip_get = self.mox.CreateMockAnything()
|
||||
api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\
|
||||
AndReturn(self.floating_ip)
|
||||
self.mox.ReplayAll()
|
||||
res = self.client.post(
|
||||
reverse('horizon:nova:access_and_security:floating_ips:disassociate',
|
||||
args=[1]),
|
||||
{'floating_ip_id': self.floating_ip.id,
|
||||
'method': 'FloatingIpDisassociate'})
|
||||
self.assertRedirects(res, FLOATING_IPS_INDEX)
|
||||
|
||||
action = "floating_ips__disassociate__%s" % self.floating_ip.id
|
||||
res = self.client.post(INDEX_URL, {"action": action})
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
def test_disassociate_post_with_exception(self):
|
||||
self.mox.StubOutWithMock(api, 'tenant_floating_ip_list')
|
||||
api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.floating_ips)
|
||||
self.mox.StubOutWithMock(api.nova, 'keypair_list')
|
||||
self.mox.StubOutWithMock(api, 'security_group_list')
|
||||
self.mox.StubOutWithMock(api, 'tenant_floating_ip_list')
|
||||
self.mox.StubOutWithMock(api, 'tenant_floating_ip_get')
|
||||
self.mox.StubOutWithMock(api, 'server_remove_floating_ip')
|
||||
|
||||
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||
api.security_group_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.security_groups)
|
||||
self.mox.StubOutWithMock(api.nova, 'keypair_list')
|
||||
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||
api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.floating_ips)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'server_remove_floating_ip')
|
||||
exception = novaclient_exceptions.ClientException('ClientException',
|
||||
message='clientException')
|
||||
api.server_remove_floating_ip(IsA(http.HttpRequest),
|
||||
IsA(int),
|
||||
IsA(int)).AndRaise(exception)
|
||||
self.mox.StubOutWithMock(api, 'tenant_floating_ip_get')
|
||||
api.tenant_floating_ip_get = self.mox.CreateMockAnything()
|
||||
api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\
|
||||
AndReturn(self.floating_ip)
|
||||
self.mox.ReplayAll()
|
||||
res = self.client.post(
|
||||
reverse('horizon:nova:access_and_security:floating_ips:disassociate',
|
||||
args=[1]),
|
||||
{'floating_ip_id': self.floating_ip.id,
|
||||
'method': 'FloatingIpDisassociate'})
|
||||
|
||||
action = "floating_ips__disassociate__%s" % self.floating_ip.id
|
||||
res = self.client.post(INDEX_URL, {"action": action})
|
||||
self.assertRaises(novaclient_exceptions.ClientException)
|
||||
self.assertRedirects(res, FLOATING_IPS_INDEX)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
|
|
@ -20,10 +20,11 @@
|
|||
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
from .views import AssociateView
|
||||
|
||||
urlpatterns = patterns(
|
||||
'horizon.dashboards.nova.access_and_security.floating_ips.views',
|
||||
url(r'^$', 'index', name='index'),
|
||||
url(r'^(?P<ip_id>[^/]+)/associate/$', 'associate', name='associate'),
|
||||
url(r'^(?P<ip_id>[^/]+)/disassociate/$', 'disassociate',
|
||||
name='disassociate'))
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^(?P<ip_id>[^/]+)/associate/$',
|
||||
AssociateView.as_view(),
|
||||
name='associate')
|
||||
)
|
||||
|
|
|
@ -23,76 +23,36 @@ Views for managing Nova floating IPs.
|
|||
"""
|
||||
import logging
|
||||
|
||||
from django import template
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django import shortcuts
|
||||
from django.utils.translation import ugettext as _
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
|
||||
from horizon import api
|
||||
from horizon.dashboards.nova.access_and_security.floating_ips.forms import \
|
||||
(ReleaseFloatingIp, FloatingIpAssociate,
|
||||
FloatingIpDisassociate, FloatingIpAllocate)
|
||||
from horizon import forms
|
||||
from .forms import FloatingIpAssociate
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
for f in (ReleaseFloatingIp, FloatingIpDisassociate, FloatingIpAllocate):
|
||||
_unused, handled = f.maybe_handle(request)
|
||||
if handled:
|
||||
return handled
|
||||
try:
|
||||
floating_ips = api.tenant_floating_ip_list(request)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
floating_ips = []
|
||||
LOG.exception("ClientException in floating ip index")
|
||||
messages.error(request,
|
||||
_('Error fetching floating ips: %s') % e.message)
|
||||
allocate_form = FloatingIpAllocate(initial={
|
||||
'tenant_id': request.user.tenant_id})
|
||||
return shortcuts.render(request,
|
||||
'nova/access_and_security/floating_ips/index.html', {
|
||||
'allocate_form': allocate_form,
|
||||
'disassociate_form': FloatingIpDisassociate(),
|
||||
'floating_ips': floating_ips,
|
||||
'release_form': ReleaseFloatingIp()})
|
||||
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 = kwargs['ip_id']
|
||||
try:
|
||||
return api.tenant_floating_ip_get(self.request, ip_id)
|
||||
except Exception as e:
|
||||
LOG.exception('Error fetching floating ip with id "%s".' % ip_id)
|
||||
messages.error(self.request,
|
||||
_('Unable to associate floating ip: %s') % e)
|
||||
raise http.Http404("Floating IP %s not available." % ip_id)
|
||||
|
||||
@login_required
|
||||
def associate(request, ip_id):
|
||||
instancelist = [(server.id, 'id: %s, name: %s' %
|
||||
(server.id, server.name))
|
||||
for server in api.server_list(request)]
|
||||
|
||||
form, handled = FloatingIpAssociate().maybe_handle(request, initial={
|
||||
'floating_ip_id': ip_id,
|
||||
'floating_ip': api.tenant_floating_ip_get(request, ip_id).ip,
|
||||
'instances': instancelist})
|
||||
if handled:
|
||||
return handled
|
||||
|
||||
context = {'floating_ip_id': ip_id,
|
||||
'form': form}
|
||||
|
||||
if request.is_ajax():
|
||||
template = 'nova/access_and_security/floating_ips/_associate.html'
|
||||
context['hide'] = True
|
||||
else:
|
||||
template = 'nova/access_and_security/floating_ips/associate.html'
|
||||
|
||||
return shortcuts.render(request, template, context)
|
||||
|
||||
|
||||
@login_required
|
||||
def disassociate(request, ip_id):
|
||||
form, handled = FloatingIpDisassociate().maybe_handle(request)
|
||||
if handled:
|
||||
return handled
|
||||
|
||||
return shortcuts.render(request,
|
||||
'nova/access_and_security/floating_ips/associate.html', {
|
||||
'floating_ip_id': ip_id})
|
||||
def get_initial(self):
|
||||
instances = [(server.id, 'id: %s, name: %s' %
|
||||
(server.id, server.name))
|
||||
for server in api.server_list(self.request)]
|
||||
return {'floating_ip_id': self.object.id,
|
||||
'floating_ip': self.object.ip,
|
||||
'instances': instances}
|
||||
|
|
|
@ -36,24 +36,7 @@ from horizon import forms
|
|||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeleteKeypair(forms.SelfHandlingForm):
|
||||
keypair_id = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
LOG.info('Deleting keypair "%s"' % data['keypair_id'])
|
||||
api.keypair_delete(request, urlquote(data['keypair_id']))
|
||||
messages.info(request, _('Successfully deleted keypair: %s')
|
||||
% data['keypair_id'])
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in DeleteKeypair")
|
||||
messages.error(request,
|
||||
_('Error deleting keypair: %s') % e.message)
|
||||
return shortcuts.redirect(request.build_absolute_uri())
|
||||
|
||||
|
||||
class CreateKeypair(forms.SelfHandlingForm):
|
||||
|
||||
name = forms.CharField(max_length="20",
|
||||
label=_("Keypair Name"),
|
||||
validators=[validators.validate_slug],
|
||||
|
@ -79,7 +62,6 @@ class CreateKeypair(forms.SelfHandlingForm):
|
|||
|
||||
|
||||
class ImportKeypair(forms.SelfHandlingForm):
|
||||
|
||||
name = forms.CharField(max_length="20", label=_("Keypair Name"),
|
||||
validators=[validators.RegexValidator('\w+')])
|
||||
public_key = forms.CharField(label=_("Public Key"), widget=forms.Textarea)
|
||||
|
|
|
@ -40,11 +40,13 @@ class KeyPairViewTests(test.BaseViewTests):
|
|||
|
||||
def test_delete_keypair(self):
|
||||
KEYPAIR_ID = self.keypairs[0].name
|
||||
formData = {'method': 'DeleteKeypair',
|
||||
'keypair_id': KEYPAIR_ID}
|
||||
formData = {'action': 'keypairs__delete__%s' % KEYPAIR_ID}
|
||||
|
||||
self.mox.StubOutWithMock(api, 'keypair_delete')
|
||||
api.keypair_delete(IsA(http.HttpRequest), unicode(KEYPAIR_ID))
|
||||
self.mox.StubOutWithMock(api.nova, 'keypair_list')
|
||||
self.mox.StubOutWithMock(api.nova, 'keypair_delete')
|
||||
|
||||
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||
api.nova.keypair_delete(IsA(http.HttpRequest), unicode(KEYPAIR_ID))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
|
@ -54,15 +56,16 @@ class KeyPairViewTests(test.BaseViewTests):
|
|||
|
||||
def test_delete_keypair_exception(self):
|
||||
KEYPAIR_ID = self.keypairs[0].name
|
||||
formData = {'method': 'DeleteKeypair',
|
||||
'keypair_id': KEYPAIR_ID,
|
||||
}
|
||||
formData = {'action': 'keypairs__delete__%s' % KEYPAIR_ID}
|
||||
|
||||
self.mox.StubOutWithMock(api.nova, 'keypair_list')
|
||||
self.mox.StubOutWithMock(api.nova, 'keypair_delete')
|
||||
|
||||
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||
exception = novaclient_exceptions.ClientException('clientException',
|
||||
message='clientException')
|
||||
self.mox.StubOutWithMock(api, 'keypair_delete')
|
||||
api.keypair_delete(IsA(http.HttpRequest),
|
||||
unicode(KEYPAIR_ID)).AndRaise(exception)
|
||||
api.nova.keypair_delete(IsA(http.HttpRequest), unicode(KEYPAIR_ID)) \
|
||||
.AndRaise(exception)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
|
|
|
@ -20,12 +20,10 @@
|
|||
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
from .views import IndexView, CreateView, ImportView
|
||||
from .views import CreateView, ImportView
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'horizon.dashboards.nova.access_and_security.keypairs.views',
|
||||
url(r'^$', IndexView.as_view(), name='index'),
|
||||
urlpatterns = patterns('',
|
||||
url(r'^create/$', CreateView.as_view(), name='create'),
|
||||
url(r'^import/$', ImportView.as_view(), name='import'),
|
||||
)
|
||||
|
|
|
@ -23,38 +23,13 @@ Views for managing Nova keypairs.
|
|||
"""
|
||||
import logging
|
||||
|
||||
from django import http
|
||||
from django import shortcuts
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.translation import ugettext as _
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
|
||||
from horizon import api
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
from .forms import CreateKeypair, DeleteKeypair, ImportKeypair
|
||||
from .tables import KeypairsTable
|
||||
from .forms import CreateKeypair, ImportKeypair
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexView(tables.DataTableView):
|
||||
table_class = KeypairsTable
|
||||
template_name = 'nova/access_and_security/keypairs/index.html'
|
||||
|
||||
def get_data(self):
|
||||
try:
|
||||
keypairs = api.nova.keypair_list(self.request)
|
||||
except Exception, e:
|
||||
keypairs = []
|
||||
LOG.exception("ClientException in keypair index")
|
||||
messages.error(request,
|
||||
_('Error fetching keypairs: %s') % e.message)
|
||||
return keypairs
|
||||
|
||||
|
||||
class CreateView(forms.ModalFormView):
|
||||
form_class = CreateKeypair
|
||||
template_name = 'nova/access_and_security/keypairs/create.html'
|
||||
|
|
|
@ -56,27 +56,6 @@ class CreateGroup(forms.SelfHandlingForm):
|
|||
e.message)
|
||||
|
||||
|
||||
class DeleteGroup(forms.SelfHandlingForm):
|
||||
tenant_id = forms.CharField(widget=forms.HiddenInput())
|
||||
security_group_id = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
LOG.info('Delete security_group: "%s"' % data)
|
||||
|
||||
security_group = api.security_group_delete(request,
|
||||
data['security_group_id'])
|
||||
messages.success(request,
|
||||
_('Successfully deleted security_group: %s')
|
||||
% data['security_group_id'])
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in DeleteGroup")
|
||||
messages.error(request, _('Error deleting security group: %s')
|
||||
% e.message)
|
||||
return shortcuts.redirect(
|
||||
'horizon:nova:access_and_security:index')
|
||||
|
||||
|
||||
class AddRule(forms.SelfHandlingForm):
|
||||
ip_protocol = forms.ChoiceField(choices=[('tcp', 'tcp'),
|
||||
('udp', 'udp'),
|
||||
|
@ -108,25 +87,3 @@ class AddRule(forms.SelfHandlingForm):
|
|||
messages.error(request, _('Error adding rule security group: %s')
|
||||
% e.message)
|
||||
return shortcuts.redirect(request.build_absolute_uri())
|
||||
|
||||
|
||||
class DeleteRule(forms.SelfHandlingForm):
|
||||
security_group_rule_id = forms.CharField(widget=forms.HiddenInput())
|
||||
tenant_id = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
def handle(self, request, data):
|
||||
security_group_rule_id = data['security_group_rule_id']
|
||||
tenant_id = data['tenant_id']
|
||||
try:
|
||||
LOG.info('Delete security_group_rule: "%s"' % data)
|
||||
|
||||
security_group = api.security_group_rule_delete(
|
||||
request,
|
||||
security_group_rule_id)
|
||||
messages.info(request, _('Successfully deleted rule: %s')
|
||||
% security_group_rule_id)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in DeleteRule")
|
||||
messages.error(request, _('Error authorizing security group: %s')
|
||||
% e.message)
|
||||
return shortcuts.redirect(request.build_absolute_uri())
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 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.
|
||||
|
||||
import logging
|
||||
|
||||
from django import shortcuts
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
|
||||
from horizon import api
|
||||
from horizon import tables
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeleteGroup(tables.Action):
|
||||
name = "delete"
|
||||
verbose_name = _("Delete")
|
||||
verbose_name_plural = _("Delete Security Groups")
|
||||
classes = ('danger',)
|
||||
|
||||
def allowed(self, request, security_group=None):
|
||||
if not security_group:
|
||||
return True
|
||||
return security_group.name != 'default'
|
||||
|
||||
def handle(self, table, request, object_ids):
|
||||
tenant_id = request.user.tenant_id
|
||||
deleted = []
|
||||
for obj_id in object_ids:
|
||||
obj = table.get_object_by_id(int(obj_id))
|
||||
if obj.name == "default":
|
||||
messages.info(request, _("The default group can't be deleted"))
|
||||
continue
|
||||
try:
|
||||
security_group = api.security_group_delete(request, obj_id)
|
||||
deleted.append(obj)
|
||||
LOG.info('Deleted security_group: "%s"' % obj.name)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("Error deleting security group")
|
||||
messages.error(request, _('Unable to delete group: %s')
|
||||
% obj.name)
|
||||
if deleted:
|
||||
messages.success(request,
|
||||
_('Successfully deleted security groups: %s')
|
||||
% ", ".join([group.name for group in deleted]))
|
||||
return shortcuts.redirect('horizon:nova:access_and_security:index')
|
||||
|
||||
|
||||
class CreateGroup(tables.LinkAction):
|
||||
name = "create"
|
||||
verbose_name = _("Create Security Group")
|
||||
url = "horizon:nova:access_and_security:security_groups:create"
|
||||
attrs = {"class": "btn small ajax-modal"}
|
||||
|
||||
|
||||
class EditRules(tables.LinkAction):
|
||||
name = "edit_rules"
|
||||
verbose_name = _("Edit Rules")
|
||||
url = "horizon:nova:access_and_security:security_groups:edit_rules"
|
||||
|
||||
|
||||
class SecurityGroupsTable(tables.DataTable):
|
||||
name = tables.Column("name")
|
||||
description = tables.Column("description")
|
||||
|
||||
class Meta:
|
||||
name = "security_groups"
|
||||
verbose_name = _("Security Groups")
|
||||
table_actions = (CreateGroup, DeleteGroup)
|
||||
row_actions = (EditRules, DeleteGroup)
|
||||
|
||||
|
||||
class DeleteRule(tables.Action):
|
||||
name = "delete"
|
||||
verbose_name = _("Delete")
|
||||
classes = ('danger',)
|
||||
|
||||
def single(self, table, request, obj_id):
|
||||
tenant_id = request.user.tenant_id
|
||||
obj = table.get_object_by_id(int(obj_id))
|
||||
try:
|
||||
LOG.info('Delete security_group_rule: "%s"' % obj_id)
|
||||
security_group = api.security_group_rule_delete(request, obj_id)
|
||||
messages.info(request, _('Successfully deleted rule: %s')
|
||||
% obj_id)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in DeleteRule")
|
||||
messages.error(request, _('Error authorizing security group: %s')
|
||||
% e.message)
|
||||
return shortcuts.redirect('horizon:nova:access_and_security:'
|
||||
'security_groups:edit_rules',
|
||||
(obj.parent_group_id))
|
||||
|
||||
|
||||
def get_cidr(rule):
|
||||
return rule.ip_range['cidr']
|
||||
|
||||
|
||||
class RulesTable(tables.DataTable):
|
||||
protocol = tables.Column("ip_protocol", verbose_name=_("IP Protocol"))
|
||||
from_port = tables.Column("from_port", verbose_name=_("From Port"))
|
||||
to_port = tables.Column("to_port", verbose_name=_("To Port"))
|
||||
cidr = tables.Column(get_cidr, verbose_name=_("CIDR"))
|
||||
|
||||
class Meta:
|
||||
name = "rules"
|
||||
verbose_name = _("Security Group Rules")
|
||||
row_actions = (DeleteRule,)
|
|
@ -27,9 +27,11 @@ from mox import IgnoreArg, IsA
|
|||
|
||||
from horizon import api
|
||||
from horizon import test
|
||||
from .tables import SecurityGroupsTable, RulesTable
|
||||
|
||||
SECGROUP_ID = '1'
|
||||
SG_INDEX_URL = reverse('horizon:nova:access_and_security:index')
|
||||
|
||||
SECGROUP_ID = '2'
|
||||
INDEX_URL = reverse('horizon:nova:access_and_security:index')
|
||||
SG_CREATE_URL = \
|
||||
reverse('horizon:nova:access_and_security:security_groups:create')
|
||||
SG_EDIT_RULE_URL = \
|
||||
|
@ -41,10 +43,24 @@ class SecurityGroupsViewTests(test.BaseViewTests):
|
|||
def setUp(self):
|
||||
super(SecurityGroupsViewTests, self).setUp()
|
||||
|
||||
security_group = api.SecurityGroup(None)
|
||||
security_group.id = '1'
|
||||
security_group.name = 'default'
|
||||
self.security_groups = (security_group,)
|
||||
sg1 = api.SecurityGroup(None)
|
||||
sg1.id = 1
|
||||
sg1.name = 'default'
|
||||
|
||||
sg2 = api.SecurityGroup(None)
|
||||
sg2.id = 2
|
||||
sg2.name = 'group_2'
|
||||
|
||||
rule = {'id': 1,
|
||||
'ip_protocol': "tcp",
|
||||
'from_port': "80",
|
||||
'to_port': "80",
|
||||
'parent_group_id': "2",
|
||||
'ip_range': {'cidr': "0.0.0.0/32"}}
|
||||
self.rules = [api.nova.SecurityGroupRule(rule)]
|
||||
sg2.rules = self.rules
|
||||
|
||||
self.security_groups = (sg1, sg2)
|
||||
|
||||
def test_create_security_groups_get(self):
|
||||
res = self.client.get(SG_CREATE_URL)
|
||||
|
@ -73,7 +89,7 @@ class SecurityGroupsViewTests(test.BaseViewTests):
|
|||
|
||||
res = self.client.post(SG_CREATE_URL, formData)
|
||||
|
||||
self.assertRedirectsNoFollow(res, SG_INDEX_URL)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
def test_create_security_groups_post_exception(self):
|
||||
SECGROUP_NAME = 'fakegroup'
|
||||
|
@ -100,10 +116,9 @@ class SecurityGroupsViewTests(test.BaseViewTests):
|
|||
'nova/access_and_security/security_groups/create.html')
|
||||
|
||||
def test_edit_rules_get(self):
|
||||
|
||||
self.mox.StubOutWithMock(api, 'security_group_get')
|
||||
api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID).AndReturn(
|
||||
self.security_groups[0])
|
||||
self.security_groups[1])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
|
@ -112,21 +127,21 @@ class SecurityGroupsViewTests(test.BaseViewTests):
|
|||
self.assertTemplateUsed(res,
|
||||
'nova/access_and_security/security_groups/edit_rules.html')
|
||||
self.assertItemsEqual(res.context['security_group'].name,
|
||||
self.security_groups[0].name)
|
||||
self.security_groups[1].name)
|
||||
|
||||
def test_edit_rules_get_exception(self):
|
||||
exception = novaclient_exceptions.ClientException('ClientException',
|
||||
message='ClientException')
|
||||
|
||||
self.mox.StubOutWithMock(api, 'security_group_get')
|
||||
api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID).AndRaise(
|
||||
exception)
|
||||
api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID) \
|
||||
.AndRaise(exception)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(SG_EDIT_RULE_URL)
|
||||
|
||||
self.assertRedirectsNoFollow(res, SG_INDEX_URL)
|
||||
self.assertRedirects(res, INDEX_URL)
|
||||
|
||||
def test_edit_rules_add_rule(self):
|
||||
RULE_ID = '1'
|
||||
|
@ -194,72 +209,62 @@ class SecurityGroupsViewTests(test.BaseViewTests):
|
|||
def test_edit_rules_delete_rule(self):
|
||||
RULE_ID = '1'
|
||||
|
||||
formData = {'method': 'DeleteRule',
|
||||
'tenant_id': self.TEST_TENANT,
|
||||
'security_group_rule_id': RULE_ID,
|
||||
}
|
||||
|
||||
self.mox.StubOutWithMock(api, 'security_group_rule_delete')
|
||||
api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(SG_EDIT_RULE_URL, formData)
|
||||
form_data = {"action": "rules__delete__%s" % RULE_ID}
|
||||
req = self.factory.post(SG_EDIT_RULE_URL, form_data)
|
||||
table = RulesTable(req, self.rules)
|
||||
handled = table.maybe_handle()
|
||||
|
||||
self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL)
|
||||
self.assertEqual(handled['location'], SG_EDIT_RULE_URL)
|
||||
|
||||
def test_edit_rules_delete_rule_exception(self):
|
||||
exception = novaclient_exceptions.ClientException('ClientException',
|
||||
message='ClientException')
|
||||
|
||||
RULE_ID = '1'
|
||||
|
||||
formData = {'method': 'DeleteRule',
|
||||
'tenant_id': self.TEST_TENANT,
|
||||
'security_group_rule_id': RULE_ID,
|
||||
}
|
||||
|
||||
self.mox.StubOutWithMock(api, 'security_group_rule_delete')
|
||||
api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID).\
|
||||
AndRaise(exception)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(SG_EDIT_RULE_URL, formData)
|
||||
|
||||
self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL)
|
||||
|
||||
def test_delete_group(self):
|
||||
|
||||
formData = {'method': 'DeleteGroup',
|
||||
'tenant_id': self.TEST_TENANT,
|
||||
'security_group_id': SECGROUP_ID,
|
||||
}
|
||||
|
||||
self.mox.StubOutWithMock(api, 'security_group_delete')
|
||||
api.security_group_delete(IsA(http.HttpRequest), SECGROUP_ID)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(SG_INDEX_URL, formData)
|
||||
|
||||
self.assertRedirectsNoFollow(res, SG_INDEX_URL)
|
||||
|
||||
def test_delete_group_exception(self):
|
||||
exception = novaclient_exceptions.ClientException('ClientException',
|
||||
message='ClientException')
|
||||
api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID) \
|
||||
.AndRaise(exception)
|
||||
|
||||
formData = {'method': 'DeleteGroup',
|
||||
'tenant_id': self.TEST_TENANT,
|
||||
'security_group_id': SECGROUP_ID,
|
||||
}
|
||||
self.mox.ReplayAll()
|
||||
|
||||
form_data = {"action": "rules__delete__%s" % RULE_ID}
|
||||
req = self.factory.post(SG_EDIT_RULE_URL, form_data)
|
||||
table = RulesTable(req, self.rules)
|
||||
handled = table.maybe_handle()
|
||||
|
||||
self.assertEqual(handled['location'], SG_EDIT_RULE_URL)
|
||||
|
||||
def test_delete_group(self):
|
||||
self.mox.StubOutWithMock(api, 'security_group_delete')
|
||||
api.security_group_delete(IsA(http.HttpRequest), SECGROUP_ID).\
|
||||
api.security_group_delete(IsA(http.HttpRequest), '2')
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
form_data = {"action": "security_groups__delete__%s" % '2'}
|
||||
req = self.factory.post(INDEX_URL, form_data)
|
||||
table = SecurityGroupsTable(req, self.security_groups)
|
||||
handled = table.maybe_handle()
|
||||
|
||||
self.assertEqual(handled['location'], INDEX_URL)
|
||||
|
||||
def test_delete_group_exception(self):
|
||||
self.mox.StubOutWithMock(api, 'security_group_delete')
|
||||
exception = novaclient_exceptions.ClientException('ClientException',
|
||||
message='ClientException')
|
||||
api.security_group_delete(IsA(http.HttpRequest), '2').\
|
||||
AndRaise(exception)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(SG_INDEX_URL, formData)
|
||||
form_data = {"action": "security_groups__delete__%s" % '2'}
|
||||
req = self.factory.post(INDEX_URL, form_data)
|
||||
table = SecurityGroupsTable(req, self.security_groups)
|
||||
handled = table.maybe_handle()
|
||||
|
||||
self.assertRedirectsNoFollow(res, SG_INDEX_URL)
|
||||
self.assertEqual(handled['location'], INDEX_URL)
|
||||
|
|
|
@ -20,10 +20,11 @@
|
|||
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
from .views import CreateView, EditRulesView
|
||||
|
||||
urlpatterns = patterns(
|
||||
'horizon.dashboards.nova.access_and_security.security_groups.views',
|
||||
url(r'^$', 'index', name='index'),
|
||||
url(r'^create/$', 'create', name='create'),
|
||||
url(r'^(?P<security_group_id>[^/]+)/edit_rules/$', 'edit_rules',
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^create/$', CreateView.as_view(), name='create'),
|
||||
url(r'^(?P<security_group_id>[^/]+)/edit_rules/$',
|
||||
EditRulesView.as_view(),
|
||||
name='edit_rules'))
|
||||
|
|
|
@ -24,81 +24,67 @@ Views for managing Nova instances.
|
|||
import logging
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django import shortcuts
|
||||
from django.utils.translation import ugettext as _
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
|
||||
from horizon import api
|
||||
from horizon.dashboards.nova.access_and_security.security_groups.forms import \
|
||||
(CreateGroup, DeleteGroup, AddRule, DeleteRule)
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
from .forms import CreateGroup, AddRule
|
||||
from .tables import RulesTable
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
tenant_id = request.user.tenant_id
|
||||
delete_form, handled = DeleteGroup.maybe_handle(request,
|
||||
initial={'tenant_id': tenant_id})
|
||||
form = CreateGroup(initial={'tenant_id': tenant_id})
|
||||
if handled:
|
||||
return handled
|
||||
class EditRulesView(tables.DataTableView):
|
||||
table_class = RulesTable
|
||||
template_name = 'nova/access_and_security/security_groups/edit_rules.html'
|
||||
|
||||
try:
|
||||
security_groups = api.security_group_list(request)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
security_groups = []
|
||||
LOG.exception("ClientException in security_groups index")
|
||||
messages.error(request, _('Error fetching security_groups: %s')
|
||||
% e.message)
|
||||
def get_data(self):
|
||||
security_group_id = self.kwargs['security_group_id']
|
||||
try:
|
||||
self.object = api.security_group_get(self.request,
|
||||
security_group_id)
|
||||
rules = [api.nova.SecurityGroupRule(rule) for
|
||||
rule in self.object.rules]
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
self.object = None
|
||||
rules = []
|
||||
LOG.exception("ClientException in security_groups rules edit")
|
||||
messages.error(self.request,
|
||||
_('Error getting security_group: %s') % e)
|
||||
return rules
|
||||
|
||||
return shortcuts.render(request,
|
||||
'nova/access_and_security/security_groups/index.html', {
|
||||
'security_groups': security_groups,
|
||||
'form': form,
|
||||
'delete_form': delete_form})
|
||||
def handle_form(self):
|
||||
tenant_id = self.request.user.tenant_id
|
||||
initial = {'tenant_id': tenant_id,
|
||||
'security_group_id': self.kwargs['security_group_id']}
|
||||
return AddRule.maybe_handle(self.request, initial=initial)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
form, handled = self.handle_form()
|
||||
if handled:
|
||||
return handled
|
||||
tables = self.get_tables()
|
||||
if not self.object:
|
||||
return shortcuts.redirect("horizon:nova:access_and_security:index")
|
||||
context = self.get_context_data(**kwargs)
|
||||
context['form'] = form
|
||||
context['security_group'] = self.object
|
||||
return self.render_to_response(context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form, handled = self.handle_form()
|
||||
if handled:
|
||||
return handled
|
||||
return super(EditRulesView, self).post(request, *args, **kwargs)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_rules(request, security_group_id):
|
||||
tenant_id = request.user.tenant_id
|
||||
add_form, handled = AddRule.maybe_handle(request,
|
||||
initial={'tenant_id': tenant_id,
|
||||
'security_group_id': security_group_id})
|
||||
if handled:
|
||||
return handled
|
||||
class CreateView(forms.ModalFormView):
|
||||
form_class = CreateGroup
|
||||
template_name = 'nova/access_and_security/security_groups/create.html'
|
||||
|
||||
delete_form, handled = DeleteRule.maybe_handle(request,
|
||||
initial={'tenant_id': tenant_id,
|
||||
'security_group_id': security_group_id})
|
||||
if handled:
|
||||
return handled
|
||||
|
||||
try:
|
||||
security_group = api.security_group_get(request, security_group_id)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in security_groups rules edit")
|
||||
messages.error(request, _('Error getting security_group: %s')
|
||||
% e.message)
|
||||
return shortcuts.redirect('horizon:nova:access_and_security:index')
|
||||
|
||||
return shortcuts.render(request,
|
||||
'nova/access_and_security/security_groups/edit_rules.html', {
|
||||
'security_group': security_group,
|
||||
'delete_form': delete_form,
|
||||
'form': add_form})
|
||||
|
||||
|
||||
@login_required
|
||||
def create(request):
|
||||
tenant_id = request.user.tenant_id
|
||||
form, handled = CreateGroup.maybe_handle(request,
|
||||
initial={'tenant_id': tenant_id})
|
||||
if handled:
|
||||
return handled
|
||||
|
||||
return shortcuts.render(request,
|
||||
'nova/access_and_security/security_groups/create.html', {
|
||||
'form': form})
|
||||
def get_initial(self):
|
||||
return {"tenant_id": self.request.user.tenant_id}
|
||||
|
|
|
@ -74,6 +74,7 @@ class AccessAndSecurityTests(test.BaseViewTests):
|
|||
self.assertTemplateUsed(res, 'nova/access_and_security/index.html')
|
||||
self.assertItemsEqual(res.context['keypairs_table'].data,
|
||||
self.keypairs)
|
||||
self.assertItemsEqual(res.context['security_groups'],
|
||||
self.assertItemsEqual(res.context['security_groups_table'].data,
|
||||
self.security_groups)
|
||||
self.assertItemsEqual(res.context['floating_ips'], self.floating_ips)
|
||||
self.assertItemsEqual(res.context['floating_ips_table'].data,
|
||||
self.floating_ips)
|
||||
|
|
|
@ -20,17 +20,13 @@
|
|||
|
||||
from django.conf.urls.defaults import *
|
||||
|
||||
import horizon
|
||||
from .floating_ips import urls as fip_urls
|
||||
from .keypairs import urls as keypair_urls
|
||||
from .security_groups import urls as sec_group_urls
|
||||
from .views import IndexView
|
||||
|
||||
from horizon.dashboards.nova.access_and_security.keypairs import urls \
|
||||
as keypair_urls
|
||||
from horizon.dashboards.nova.access_and_security.floating_ips import urls \
|
||||
as fip_urls
|
||||
from horizon.dashboards.nova.access_and_security.security_groups import urls \
|
||||
as sec_group_urls
|
||||
|
||||
urlpatterns = patterns('horizon.dashboards.nova.access_and_security',
|
||||
url(r'^$', 'views.index', name='index'),
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', IndexView.as_view(), name='index'),
|
||||
url(r'keypairs/', include(keypair_urls, namespace='keypairs')),
|
||||
url(r'floating_ips/', include(fip_urls, namespace='floating_ips')),
|
||||
url(r'security_groups/',
|
||||
|
|
|
@ -22,86 +22,52 @@
|
|||
"""
|
||||
Views for Instances and Volumes.
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from django import http
|
||||
from django import shortcuts
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.translation import ugettext as _
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
import openstackx.api.exceptions as api_exceptions
|
||||
|
||||
from horizon import api
|
||||
from horizon import forms
|
||||
from horizon import test
|
||||
from .keypairs.forms import DeleteKeypair
|
||||
from horizon import tables
|
||||
from .keypairs.tables import KeypairsTable
|
||||
from .security_groups.forms import CreateGroup, DeleteGroup
|
||||
from .floating_ips.forms import (ReleaseFloatingIp, FloatingIpDisassociate,
|
||||
FloatingIpAllocate)
|
||||
from .floating_ips.tables import FloatingIPsTable
|
||||
from .security_groups.tables import SecurityGroupsTable
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
tenant_id = request.user.tenant_id
|
||||
class IndexView(tables.MultiTableView):
|
||||
table_classes = (KeypairsTable, SecurityGroupsTable, FloatingIPsTable)
|
||||
template_name = 'nova/access_and_security/index.html'
|
||||
|
||||
for f in (CreateGroup, DeleteGroup, DeleteKeypair, ReleaseFloatingIp,
|
||||
FloatingIpDisassociate, FloatingIpAllocate):
|
||||
_unused, handled = f.maybe_handle(request)
|
||||
if handled:
|
||||
return handled
|
||||
def get_keypairs_data(self):
|
||||
try:
|
||||
keypairs = api.nova.keypair_list(self.request)
|
||||
except Exception, e:
|
||||
keypairs = []
|
||||
LOG.exception("Exception in keypair index")
|
||||
messages.error(self.request,
|
||||
_('Keypair list is currently unavailable.'))
|
||||
return keypairs
|
||||
|
||||
# NOTE(gabriel): This is all temporary until all tables
|
||||
# in this view are converted to DataTables.
|
||||
try:
|
||||
keypairs = api.nova.keypair_list(request)
|
||||
except Exception, e:
|
||||
keypairs = []
|
||||
LOG.exception("Exception in keypair index")
|
||||
messages.error(request,
|
||||
_('Keypair list is currently unavailable.'))
|
||||
keypairs_table = KeypairsTable(request, keypairs)
|
||||
handled = keypairs_table.maybe_handle()
|
||||
if handled:
|
||||
return handled
|
||||
def get_security_groups_data(self):
|
||||
try:
|
||||
security_groups = api.security_group_list(self.request)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
security_groups = []
|
||||
LOG.exception("ClientException in security_groups index")
|
||||
messages.error(self.request,
|
||||
_('Error fetching security_groups: %s') % e)
|
||||
return security_groups
|
||||
|
||||
try:
|
||||
security_groups = api.security_group_list(request)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
security_groups = []
|
||||
LOG.exception("ClientException in security_groups index")
|
||||
messages.error(request, _('Error fetching security_groups: %s')
|
||||
% e.message)
|
||||
try:
|
||||
floating_ips = api.tenant_floating_ip_list(request)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
floating_ips = []
|
||||
LOG.exception("ClientException in floating ip index")
|
||||
messages.error(request,
|
||||
_('Error fetching floating ips: %s') % e.message)
|
||||
|
||||
context = {'keypairs_table': keypairs_table,
|
||||
'floating_ips': floating_ips,
|
||||
'security_groups': security_groups,
|
||||
'keypair_delete_form': DeleteKeypair(),
|
||||
'disassociate_form': FloatingIpDisassociate(),
|
||||
'release_form': ReleaseFloatingIp(),
|
||||
'allocate_form': FloatingIpAllocate(initial={
|
||||
'tenant_id': request.user.tenant_id}),
|
||||
'sec_group_create_form': CreateGroup(
|
||||
initial={'tenant_id': tenant_id}),
|
||||
'sec_group_delete_form': DeleteGroup.maybe_handle(request,
|
||||
initial={'tenant_id': tenant_id})}
|
||||
|
||||
if request.is_ajax():
|
||||
template = 'nova/access_and_security/index_ajax.html'
|
||||
context['hide'] = True
|
||||
else:
|
||||
template = 'nova/access_and_security/index.html'
|
||||
|
||||
return shortcuts.render(request, template, context)
|
||||
def get_floating_ips_data(self):
|
||||
try:
|
||||
floating_ips = api.tenant_floating_ip_list(self.request)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
floating_ips = []
|
||||
LOG.exception("ClientException in floating ip index")
|
||||
messages.error(self.request,
|
||||
_('Error fetching floating ips: %s') % e)
|
||||
return floating_ips
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{% load i18n %}
|
||||
<form id="form_allocate" action="." method="post">
|
||||
{% csrf_token %}
|
||||
{% for hidden in form.hidden_fields %}
|
||||
{{ hidden }}
|
||||
{% endfor %}
|
||||
<input class="btn small primary" type="submit" value="{% trans "Allocate IP" %}" />
|
||||
</form>
|
|
@ -2,9 +2,9 @@
|
|||
{% 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 form_action %}{% url horizon:nova:access_and_security:floating_ips:associate floating_ip.id %}{% endblock %}
|
||||
|
||||
{% block modal-header %}Associate Floating IP{% endblock %}
|
||||
{% block modal-header %}{% trans "Associate Floating IP" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
{% load i18n %}
|
||||
<form id="form_release_{{ ip.id }}" class="form-delete" method="post">
|
||||
{% csrf_token %}
|
||||
{% for hidden in form.hidden_fields %}
|
||||
{{ hidden }}
|
||||
{% endfor %}
|
||||
<input name="floating_ip_id" type="hidden" value="{{ ip.id }}" />
|
||||
<input id="delete_{{ ip.id }}" class="btn small delete" title="Floating IP: {{ ip.id }} ({{ ip.ip }})" type="submit" value="{% trans "Disassociate with instance." %}" />
|
||||
</form>
|
|
@ -1,56 +0,0 @@
|
|||
{% load i18n %}
|
||||
|
||||
<div class="table_title">
|
||||
<h3>Floating IPs</h3>
|
||||
<div class="table_actions">
|
||||
{% include "nova/access_and_security/floating_ips/_allocate.html" with form=allocate_form %}
|
||||
<div class="floating_ips table_search">
|
||||
<form action="#">
|
||||
<input class="span3" type="text">
|
||||
</form>
|
||||
</div>
|
||||
<a class="inspect" href="#">{% trans "inspect" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="floating_ips" class="zebra-striped sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>IP</th>
|
||||
<th>Instance</th>
|
||||
<th colspan="2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for ip in floating_ips %}
|
||||
<tr class="{% cycle 'odd' 'even' %}">
|
||||
<td class="select">
|
||||
<input type="checkbox" name="ip_{{ ip.id }}" value="ip_{{ ip.id }}" id="ip_select_{{ ip.id }}" />
|
||||
</td>
|
||||
<td>{{ip.ip }}</td>
|
||||
<td>
|
||||
{% if ip.fixed_ip %}
|
||||
<ul>
|
||||
<li><span>{% trans "Instance ID:" %}</span> {{ ip.instance_id }}</li>
|
||||
<li><span>{% trans "Fixed IP:" %}</span> {{ ip.fixed_ip }}</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if ip.fixed_ip %}
|
||||
{% include "nova/access_and_security/floating_ips/_disassociate.html" with form=disassociate_form %}
|
||||
{% else %}
|
||||
<a class="btn primary small ajax-modal" href="{% url horizon:nova:access_and_security:floating_ips:associate ip.id %}">{% trans "Associate to instance" %}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td id="name_{{ ip.name }}" class="actions">
|
||||
<a class="more-actions" href="#">More</a>
|
||||
<ul>
|
||||
<li class="form">{% include "nova/access_and_security/floating_ips/_release.html" with form=release_form %}</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
|
@ -1,9 +0,0 @@
|
|||
{% load i18n %}
|
||||
<form id="form_release_{{ ip.id }}" class="form-delete" method="post">
|
||||
{% csrf_token %}
|
||||
{% for hidden in form.hidden_fields %}
|
||||
{{ hidden }}
|
||||
{% endfor %}
|
||||
<input name="floating_ip_id" type="hidden" value="{{ ip.id }}" />
|
||||
<input id="delete_{{ ip.id }}" class="btn small delete" title="Floating IP: {{ ip.id }} ({{ ip.ip }})" type="submit" value="{% trans "Release" %}" />
|
||||
</form>
|
|
@ -1,22 +0,0 @@
|
|||
{% extends 'nova/base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}Floating IPs{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% url horizon:nova:access_and_security:floating_ips:index as refresh_link %}
|
||||
{# to make searchable false, just remove it from the include statement #}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Floating IPs") refresh_link=refresh_link searchable="true" %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% if floating_ips %}
|
||||
{% include 'nova/access_and_security/floating_ips/_list.html' %}
|
||||
{% else %}
|
||||
<div class="alert-message block-message info">
|
||||
<p><strong>{% trans "Info: " %}</strong>{% trans "There are currently no floating ips assigned to your tenant." %}</p>
|
||||
<div class="alert-actions">
|
||||
{% include "nova/access_and_security/floating_ips/_allocate.html" with form=allocate_form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -7,43 +7,15 @@
|
|||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
|
||||
<div id="floating_ips">
|
||||
{% if floating_ips %}
|
||||
{% include 'nova/access_and_security/floating_ips/_list.html' %}
|
||||
{% else %}
|
||||
<div class="alert-message block-message info">
|
||||
<p><strong>{% trans "Info: " %}</strong>{% trans "There are currently no floating ips assigned to your tenant." %}</p>
|
||||
<div class="alert-actions">
|
||||
{% include "nova/access_and_security/floating_ips/_allocate.html" with form=allocate_form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ floating_ips_table.render }}
|
||||
</div>
|
||||
|
||||
<div id="security_groups">
|
||||
{% url horizon:nova:access_and_security:security_groups:create as create_sec_url %}
|
||||
{% if security_groups %}
|
||||
{% include 'nova/access_and_security/security_groups/_list.html' %}
|
||||
{% else %}
|
||||
<div class="message_box info">
|
||||
<h2>{% trans "Info" %}</h2>
|
||||
<p>{% blocktrans %}There are currently no security groups. <a href='{{ create_sec_url }}'>Create A Security Group</a>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ security_groups_table.render }}
|
||||
</div>
|
||||
|
||||
<div id="keypairs">
|
||||
{% if keypairs_table.data %}
|
||||
{{ keypairs_table.render }}
|
||||
{% else %}
|
||||
<div class="alert-message block-message info">
|
||||
<p><strong>{% trans "Info: " %}</strong>{% trans "There are currently no keypairs." %}</p>
|
||||
<div class="alert-actions">
|
||||
<a id="keypairs_create_link" class="btn primary small" href="{% url horizon:nova:access_and_security:keypairs:create %}">{% trans "Create New Keypair" %}</a>
|
||||
<a id="keypairs_import_link" class="btn small" href="{% url horizon:nova:access_and_security:keypairs:import %}">{% trans "Import Keypair" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="keypairs">
|
||||
{{ keypairs_table.render }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
{% load i18n %}
|
||||
<form id="form_delete_{{ keypair.name }}" class="form-delete" method="post">
|
||||
{% csrf_token %}
|
||||
{% for hidden in form.hidden_fields %}
|
||||
{{ hidden }}
|
||||
{% endfor %}
|
||||
<input name="keypair_id" type="hidden" value="{{ keypair.name }}" />
|
||||
<input id="delete_{{ keypair.name }}" class="btn small danger delete" title="Keypair: {{ keypair.name }}" type="submit" value="{% trans "Delete" %}" />
|
||||
</form>
|
|
@ -1,26 +0,0 @@
|
|||
{% load i18n %}
|
||||
<form id="import_keypair_form" action="{% block form_action %}{% endblock %}" method="post">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
{% for hidden in form.hidden_fields %}
|
||||
{{ hidden }}
|
||||
{% endfor %}
|
||||
{% for field in form.visible_fields %}
|
||||
<div class="clearfix{% if field.errors %} error{% endif %}">
|
||||
{{ field.label_tag }}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}
|
||||
<span class="help-inline">{{ error }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
<div class="input">
|
||||
{{ field }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% block modal-footer %}
|
||||
<input type="submit" value="{% trans "Add Keypair" %}" />
|
||||
{% endblock %}
|
||||
</fieldset>
|
||||
</form>
|
|
@ -1,22 +0,0 @@
|
|||
{% extends 'nova/base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}Keypairs{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Keypairs") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% if table.data %}
|
||||
{{ table.render }}
|
||||
{% else %}
|
||||
<div class="alert-message block-message info">
|
||||
<p><strong>{% trans "Info: " %}</strong>{% trans "There are currently no keypairs." %}</p>
|
||||
<div class="alert-actions">
|
||||
<a id="keypairs_create_link" class="btn primary small ajax-modal" href="{% url horizon:nova:access_and_security:keypairs:create %}">{% trans "Create New Keypair" %}</a>
|
||||
<a id="keypairs_import_link" class="btn small ajax-modal" href="{% url horizon:nova:access_and_security:keypairs:import %}">{% trans "Import Keypair" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,9 +0,0 @@
|
|||
{% load i18n %}
|
||||
<form id="form_delete_{{ security_group.id }}" class="form-delete" method="post">
|
||||
{% csrf_token %}
|
||||
{% for hidden in form.hidden_fields %}
|
||||
{{ hidden }}
|
||||
{% endfor %}
|
||||
<input name="security_group_id" type="hidden" value="{{ security_group.id }}" />
|
||||
<input id="delete_{{ security_group.id }}" class="btn small danger delete" title="Security Group: {{ security_group.id }}" type="submit" value="{% trans "Delete" %}" />
|
||||
</form>
|
|
@ -1,6 +1,5 @@
|
|||
{% load i18n %}
|
||||
|
||||
|
||||
<div id="security_group_rule_modal" class="{% block modal_class %}modal{% if hide %} hide {% else %} static_page{% endif %}{% endblock %}">
|
||||
<div class="modal-header">
|
||||
{% if hide %}<a href="#" class="close">×</a>{% endif %}
|
||||
|
@ -8,35 +7,7 @@
|
|||
</div>
|
||||
<div class="modal-body clearfix">
|
||||
<div class="right">
|
||||
<h3> {% trans "Rules for Security Group" %} '{{security_group.name}}'</h3>
|
||||
<table id="security_groups" class="zebra-striped">
|
||||
<tr>
|
||||
<th>{% trans "IP Protocol" %}</th>
|
||||
<th>{% trans "From Port" %}</th>
|
||||
<th>{% trans "To Port" %}</th>
|
||||
<th>{% trans "CIDR" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
{% for rule in security_group.rules %}
|
||||
<tr class="{% cycle 'odd' 'even' %}">
|
||||
<td>{{ rule.ip_protocol }}</td>
|
||||
<td>{{ rule.from_port }}</td>
|
||||
<td>{{ rule.to_port }}</td>
|
||||
<td>{{ rule.ip_range.cidr }}</td>
|
||||
<td id="actions">
|
||||
<ul>
|
||||
<li class="form">{% include "nova/access_and_security/security_groups/_delete_rule.html" with form=delete_form %}</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="100%">
|
||||
{% trans "No rules for this security group" %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{{ table.render }}
|
||||
</div>
|
||||
<form id="edit_security_group_rule_form" action="{% url horizon:nova:access_and_security:security_groups:edit_rules security_group.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
|
@ -50,6 +21,3 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% block modal-footer %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
{% load i18n %}
|
||||
|
||||
<div class="table_title">
|
||||
<h3>{% trans "Security Groups" %}</h3>
|
||||
<div class="table_actions">
|
||||
|
||||
<a id="security_groups_create_link" class="btn primary small" href="{{ create_sec_url }}">{% trans "Create Security Group" %}</a>
|
||||
|
||||
<div class="security_group table_search">
|
||||
<form action="#">
|
||||
<input class="span3" type="text">
|
||||
</form>
|
||||
</div>
|
||||
<a class="inspect" href="#">{% trans "inspect" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="security_groups" class="zebra-striped sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th colspan="2">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for security_group in security_groups %}
|
||||
<tr class="{% cycle 'odd' 'even' %}">
|
||||
<td class="select">
|
||||
<input type="checkbox" name="security_group_{{ security_group.id }}" value="security_group_{{ security_group.id }}" id="security_group_select_{{ security_group.id }}" />
|
||||
</td>
|
||||
<td><a href="{% url horizon:nova:access_and_security:security_groups:edit_rules security_group.id %}">{{ security_group.name }}</a></td>
|
||||
<td>{{ security_group.description }}</td>
|
||||
<td>
|
||||
<a class="btn primary small" href="{% url horizon:nova:access_and_security:security_groups:edit_rules security_group.id %}">{% trans "Edit Rules" %}</a>
|
||||
</td>
|
||||
<td id="name_{{security_group.name}}" class="actions">
|
||||
{% if security_group.name != 'default' %}
|
||||
<a class="more-actions" href="#">More</a>
|
||||
<ul>
|
||||
<li class="form">{% include "nova/access_and_security/security_groups/_delete.html" with form=delete_form %}</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
|
@ -7,5 +7,5 @@
|
|||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% include 'nova/access_and_security/security_groups/_create.html' with form=form %}
|
||||
{% include 'nova/access_and_security/security_groups/_create.html' %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
{% extends 'nova/base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}Security Groups{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Security Groups") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% if security_groups %}
|
||||
{% url horizon:nova:access_and_security:security_groups:create as create_sec_url %}
|
||||
{% include 'nova/access_and_security/security_groups/_list.html' %}
|
||||
{% else %}
|
||||
<div class="message_box info">
|
||||
{% url horizon:nova:access_and_security:security_groups:create as dash_sec_url %}
|
||||
<h2>{% trans "Info" %}</h2>
|
||||
<p>{% blocktrans %}There are currently no security groups. <a href='{{ dash_sec_url }}' data-controls-modal="create_security_group_modal" data-backdrop="static">Create A Security Group</a>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% include 'nova/access_and_security/security_groups/_create.html' with form=form hide=True%}
|
||||
|
||||
{% endblock %}
|
|
@ -17,4 +17,4 @@
|
|||
# Convenience imports for public API components.
|
||||
from .actions import Action, LinkAction, FilterAction
|
||||
from .base import DataTable, Column
|
||||
from .views import DataTableView
|
||||
from .views import DataTableView, MultiTableView
|
||||
|
|
|
@ -30,6 +30,7 @@ class BaseAction(object):
|
|||
handles_multiple = False
|
||||
attrs = {}
|
||||
name = None
|
||||
requires_input = False
|
||||
|
||||
def allowed(self, request, datum):
|
||||
""" Determine whether this action is allowed for the current request.
|
||||
|
@ -84,6 +85,11 @@ class Action(BaseAction):
|
|||
The HTTP method for this action. Defaults to ``POST``. Other methods
|
||||
may or may not succeed currently.
|
||||
|
||||
.. attribute:: requires_input
|
||||
|
||||
Boolean value indicating whether or not this action can be taken
|
||||
without any additional input (e.g. an object id). Defaults to ``True``.
|
||||
|
||||
At least one of the following methods must be defined:
|
||||
|
||||
.. method:: single(self, data_table, request, object_id)
|
||||
|
@ -104,10 +110,11 @@ class Action(BaseAction):
|
|||
into a list containing only the single object id.
|
||||
"""
|
||||
method = "POST"
|
||||
requires_input = True
|
||||
|
||||
def __init__(self, verbose_name=None, verbose_name_plural=None,
|
||||
single_func=None, multiple_func=None, handle_func=None,
|
||||
handles_multiple=False, attrs=None):
|
||||
handles_multiple=False, attrs=None, requires_input=True):
|
||||
super(Action, self).__init__()
|
||||
self.name = unicode(getattr(self, 'name', self.__class__.__name__))
|
||||
verbose_name = verbose_name or self.name.title()
|
||||
|
@ -121,6 +128,9 @@ class Action(BaseAction):
|
|||
self.handles_multiple = getattr(self,
|
||||
"handles_multiple",
|
||||
handles_multiple)
|
||||
self.requires_input = getattr(self,
|
||||
"requires_input",
|
||||
requires_input)
|
||||
if attrs:
|
||||
self.attrs.update(attrs)
|
||||
|
||||
|
|
|
@ -260,8 +260,9 @@ class Row(object):
|
|||
for column in table.columns.values():
|
||||
if column.auto == "multi_select":
|
||||
widget = forms.CheckboxInput(check_test=False)
|
||||
# Convert value to string to avoid accidental type conversion
|
||||
data = widget.render('object_ids',
|
||||
table.get_object_id(datum))
|
||||
str(table.get_object_id(datum)))
|
||||
column._data_cache[datum] = data
|
||||
elif column.auto == "actions":
|
||||
data = table.render_row_actions(datum)
|
||||
|
@ -623,6 +624,10 @@ class DataTable(object):
|
|||
context = template.RequestContext(self._meta.request, extra_context)
|
||||
return table_template.render(context)
|
||||
|
||||
def get_empty_message(self):
|
||||
""" Returns the message to be displayed when there is no data. """
|
||||
return _("No items to display.")
|
||||
|
||||
def get_object_by_id(self, lookup):
|
||||
"""
|
||||
Returns the data object from the table's dataset which matches
|
||||
|
@ -717,7 +722,7 @@ class DataTable(object):
|
|||
# See if we have a list of ids
|
||||
obj_ids = obj_ids or self._meta.request.POST.getlist('object_ids')
|
||||
action = self.base_actions.get(action_name, None)
|
||||
if action and (obj_id or obj_ids):
|
||||
if action and (not action.requires_input or obj_id or obj_ids):
|
||||
# Single handling is easy
|
||||
if not action.handles_multiple:
|
||||
response = action.single(self, self._meta.request, obj_id)
|
||||
|
@ -727,6 +732,9 @@ class DataTable(object):
|
|||
obj_ids = [obj_id]
|
||||
response = action.multiple(self, self._meta.request, obj_ids)
|
||||
return response
|
||||
elif action and action.requires_input and not (obj_id or obj_ids):
|
||||
messages.info(self._meta.request,
|
||||
_("Please select a row before taking that action."))
|
||||
return None
|
||||
|
||||
def maybe_handle(self):
|
||||
|
|
|
@ -17,7 +17,74 @@
|
|||
from django.views import generic
|
||||
|
||||
|
||||
class DataTableView(generic.TemplateView):
|
||||
class MultiTableView(generic.TemplateView):
|
||||
"""
|
||||
A class-based generic view to handle the display and processing of
|
||||
multiple :class:`~horizon.tables.DataTable` classes in a single view.
|
||||
|
||||
Three steps are required to use this view: set the ``table_classes``
|
||||
attribute with a tuple of the desired
|
||||
:class:`~horizon.tables.DataTable` classes;
|
||||
define a ``get_{{ table_name }}_data`` method for each table class
|
||||
which returns a set of data for that table; and specify a template for
|
||||
the ``template_name`` attribute.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MultiTableView, self).__init__(*args, **kwargs)
|
||||
self.table_classes = getattr(self, "table_classes", [])
|
||||
self._data = {}
|
||||
self._tables = {}
|
||||
|
||||
def get_data(self):
|
||||
if not self._data:
|
||||
for table in self.table_classes:
|
||||
func_name = "get_%s_data" % table._meta.name
|
||||
data_func = getattr(self, func_name, None)
|
||||
if data_func is None:
|
||||
cls_name = self.__class__.__name__
|
||||
raise NotImplementedError("You must define a %s method "
|
||||
"on %s." % (func_name, cls_name))
|
||||
self._data[table._meta.name] = data_func()
|
||||
return self._data
|
||||
|
||||
def get_tables(self):
|
||||
if not self.table_classes:
|
||||
raise AttributeError('You must specify a one or more DataTable '
|
||||
'classes for the "table_classes" attribute '
|
||||
'on %s.' % self.__class__.__name__)
|
||||
if not self._tables:
|
||||
for table in self.table_classes:
|
||||
func_name = "get_%s_table" % table._meta.name
|
||||
table_func = getattr(self, func_name, None)
|
||||
data = self.get_data()[table._meta.name]
|
||||
if table_func is None:
|
||||
tbl = table(self.request, data)
|
||||
else:
|
||||
tbl = table_func(self, self.request, data)
|
||||
self._tables[table._meta.name] = tbl
|
||||
return self._tables
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(MultiTableView, self).get_context_data(**kwargs)
|
||||
tables = self.get_tables()
|
||||
for name, table in tables.items():
|
||||
context["%s_table" % name] = table
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = self.get_context_data(**kwargs)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
tables = self.get_tables().values()
|
||||
for table in tables:
|
||||
handled = table.maybe_handle()
|
||||
if handled:
|
||||
return handled
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class DataTableView(MultiTableView):
|
||||
""" A class-based generic view to handle basic DataTable processing.
|
||||
|
||||
Three steps are required to use this view: set the ``table_class``
|
||||
|
@ -32,6 +99,10 @@ class DataTableView(generic.TemplateView):
|
|||
raise NotImplementedError('You must define a "get_data" method on %s.'
|
||||
% self.__class__.__name__)
|
||||
|
||||
def get_tables(self):
|
||||
table = self.get_table()
|
||||
return {table._meta.name: table}
|
||||
|
||||
def get_table(self):
|
||||
if not self.table_class:
|
||||
raise AttributeError('You must specify a DataTable class for the '
|
||||
|
@ -41,15 +112,7 @@ class DataTableView(generic.TemplateView):
|
|||
self.table = self.table_class(self.request, self.get_data())
|
||||
return self.table
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
table = self.get_table()
|
||||
context = self.get_context_data(**kwargs)
|
||||
context[self.context_object_name] = table
|
||||
return self.render_to_response(context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
table = self.get_table()
|
||||
handled = table.maybe_handle()
|
||||
if handled:
|
||||
return handled
|
||||
return self.get(request, *args, **kwargs)
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DataTableView, self).get_context_data(**kwargs)
|
||||
context[self.context_object_name] = self.table
|
||||
return context
|
||||
|
|
|
@ -3,22 +3,33 @@
|
|||
<h3 class='table_title'>{{ table }}</h3>
|
||||
{{ table.render_table_actions }}
|
||||
</div>
|
||||
{% with columns=table.get_columns rows=table.get_rows %}
|
||||
<table id="{{ table.name }}" class="zebra-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for column in table.get_columns %}
|
||||
{% for column in columns %}
|
||||
<th class="{{ column.get_classes }}">{{ column }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in table.get_rows %}
|
||||
{% for row in rows %}
|
||||
<tr id="{{ row.id }}" class="{% cycle 'odd' 'even' %} {{ row.status_class }}">
|
||||
{% for cell in row %}
|
||||
<td class="{{ cell.get_classes }}">{{ cell.value }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr class="{% cycle 'odd' 'even' %} empty">
|
||||
<td colspan="{{ table.get_columns|length }}">{{ table.get_empty_message }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="{{ table.get_columns|length }}">Displaying {{ rows|length }} item{{ rows|pluralize }}</td>
|
||||
</td>
|
||||
</tfoot>
|
||||
</table>
|
||||
{% endwith %}
|
||||
</form>
|
||||
|
|
|
@ -25,6 +25,8 @@ from django import shortcuts
|
|||
from django import test as django_test
|
||||
from django import template as django_template
|
||||
from django.conf import settings
|
||||
from django.contrib.messages.storage import default_storage
|
||||
from django.test.client import RequestFactory
|
||||
import httplib2
|
||||
import mox
|
||||
|
||||
|
@ -60,6 +62,14 @@ def utcnow():
|
|||
utcnow.override_time = None
|
||||
|
||||
|
||||
class RequestFactoryWithMessages(RequestFactory):
|
||||
def post(self, *args, **kwargs):
|
||||
req = super(RequestFactoryWithMessages, self).post(*args, **kwargs)
|
||||
req.session = []
|
||||
req._messages = default_storage(req)
|
||||
return req
|
||||
|
||||
|
||||
class TestCase(django_test.TestCase):
|
||||
TEST_STAFF_USER = 'staffUser'
|
||||
TEST_TENANT = '1'
|
||||
|
@ -114,6 +124,7 @@ class TestCase(django_test.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
self.mox = mox.Mox()
|
||||
self.factory = RequestFactoryWithMessages()
|
||||
|
||||
def fake_conn_request(*args, **kwargs):
|
||||
raise Exception("An external URI request tried to escape through "
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
from django import http
|
||||
from django import shortcuts
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
import horizon
|
||||
from horizon import tables
|
||||
|
@ -110,13 +109,6 @@ class MyTable(tables.DataTable):
|
|||
|
||||
|
||||
class DataTableTests(test.TestCase):
|
||||
def setUp(self):
|
||||
super(DataTableTests, self).setUp()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def tearDown(self):
|
||||
super(DataTableTests, self).tearDown()
|
||||
|
||||
def test_table_instantiation(self):
|
||||
""" Tests everything that happens when the table is instantiated. """
|
||||
self.table = MyTable(self.request, TEST_DATA)
|
||||
|
@ -372,6 +364,8 @@ class DataTableTests(test.TestCase):
|
|||
('my_table', 'click', None))
|
||||
handled = self.table.maybe_handle()
|
||||
self.assertEqual(handled, None)
|
||||
self.assertEqual(list(req._messages)[0].message,
|
||||
"Please select a row before taking that action.")
|
||||
|
||||
# Filtering
|
||||
action_string = "my_table__filter__q"
|
||||
|
|
|
@ -343,6 +343,17 @@ table form {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#main_content table tr.empty td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#main_content table tfoot tr td {
|
||||
border-top: 1px solid #DDD;
|
||||
background-color: #F1F1F1;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.table_actions {
|
||||
float: right;
|
||||
min-width: 400px;
|
||||
|
|
|
@ -17,7 +17,14 @@ horizon.addInitFunction(function() {
|
|||
}
|
||||
});
|
||||
|
||||
$('table.sortable').tablesorter();
|
||||
$('table.sortable').each(function(index, table) {
|
||||
var $table = $(table);
|
||||
// Only trigger if we have actual data rows in the table.
|
||||
// Calling on an empty table throws a javascript error.
|
||||
if ($table.find('tbody tr').length) {
|
||||
$table.tablesorter();
|
||||
}
|
||||
})
|
||||
|
||||
// Actions button dropdown behavior
|
||||
$('.action.primary').mouseenter(function() {
|
||||
|
|
|
@ -267,7 +267,7 @@ function run_tests {
|
|||
|
||||
echo "Running Horizon application tests"
|
||||
${command_wrapper} coverage erase
|
||||
${command_wrapper} coverage run $root/openstack-dashboard/manage.py test horizon --settings=horizon.tests.testsettings
|
||||
${command_wrapper} coverage run $root/openstack-dashboard/manage.py test horizon --settings=horizon.tests.testsettings $testargs
|
||||
# get results of the Horizon tests
|
||||
HORIZON_RESULT=$?
|
||||
|
||||
|
@ -278,9 +278,9 @@ function run_tests {
|
|||
cp $root/openstack-dashboard/local/local_settings.py.example $root/openstack-dashboard/local/local_settings.py
|
||||
|
||||
if [ $selenium -eq 1 ]; then
|
||||
${command_wrapper} coverage run $root/openstack-dashboard/manage.py test dashboard --with-selenium --with-cherrypyliveserver
|
||||
${command_wrapper} coverage run $root/openstack-dashboard/manage.py test dashboard --with-selenium --with-cherrypyliveserver $testargs
|
||||
else
|
||||
${command_wrapper} coverage run $root/openstack-dashboard/manage.py test dashboard
|
||||
${command_wrapper} coverage run $root/openstack-dashboard/manage.py test dashboard $testargs
|
||||
fi
|
||||
# get results of the openstack-dashboard tests
|
||||
DASHBOARD_RESULT=$?
|
||||
|
|
Loading…
Reference in New Issue