Skip to content

Commit

Permalink
Converts instances and volumes to new tables, modals, etc.
Browse files Browse the repository at this point in the history
This commit reworks the instances and volumes panels, extends that to
the syspanel instances panel, cleans up usage-related code and
moves it to overview and/or tenants panels as appropriate,
and finally implements a new layout/modal interface style
for combined modal/table views like security groups and
volume attachments.

Re-ordered the attach volume form. Fixed bug 913863.
Reworked syspanel usage views. Fixed bug 904861.
Table displays have much more useful data. Fixed bug 905065 and fixed bug 907512.
New modals fixed bug 898867.

Lots of additional code cleanup and fixes.

Change-Id: I407d3ec70a080883c137a963fa0ee22124b53dc2
  • Loading branch information
gabrielhurley committed Jan 18, 2012
1 parent 29b70fb commit 6c35916
Show file tree
Hide file tree
Showing 65 changed files with 1,402 additions and 1,766 deletions.
44 changes: 33 additions & 11 deletions horizon/horizon/api/nova.py
Expand Up @@ -25,6 +25,7 @@

from django.contrib import messages
from novaclient.v1_1 import client as nova_client
from novaclient.v1_1 import security_group_rules as nova_rules
from novaclient.v1_1.servers import REBOOT_HARD

from horizon.api.base import *
Expand Down Expand Up @@ -87,7 +88,8 @@ class Server(APIResourceWrapper):
"""
_attrs = ['addresses', 'attrs', 'hostId', 'id', 'image', 'links',
'metadata', 'name', 'private_ip', 'public_ip', 'status', 'uuid',
'image_name', 'VirtualInterfaces', 'flavor', 'key_name']
'image_name', 'VirtualInterfaces', 'flavor', 'key_name',
'OS-EXT-STS:power_state', 'OS-EXT-STS:task_state']

def __init__(self, apiresource, request):
super(Server, self).__init__(apiresource)
Expand Down Expand Up @@ -135,12 +137,31 @@ class Usage(APIResourceWrapper):

class SecurityGroup(APIResourceWrapper):
"""Simple wrapper around openstackx.extras.security_groups.SecurityGroup"""
_attrs = ['id', 'name', 'description', 'tenant_id', 'rules']
_attrs = ['id', 'name', 'description', 'tenant_id']

@property
def rules(self):
""" Wraps transmitted rule info in the novaclient rule class. """
if not hasattr(self, "_rules"):
manager = nova_rules.SecurityGroupRuleManager
self._rules = [nova_rules.SecurityGroupRule(manager, rule) for \
rule in self._apiresource.rules]
return self._rules

@rules.setter
def rules(self, value):
self._rules = value


class SecurityGroupRule(APIDictWrapper):
class SecurityGroupRule(APIResourceWrapper):
""" Simple wrapper for individual rules in a SecurityGroup. """
_attrs = ['ip_protocol', 'from_port', 'to_port', 'ip_range']
_attrs = ['id', 'ip_protocol', 'from_port', 'to_port', 'ip_range']

def __unicode__(self):
vals = {'from': self.from_port,
'to': self.to_port,
'cidr': self.ip_range['cidr']}
return 'ALLOW %(from)s:%(to)s from %(cidr)s' % vals


def novaclient(request):
Expand Down Expand Up @@ -192,6 +213,7 @@ def floating_ip_pools_list(request):
return [FloatingIpPool(pool)
for pool in novaclient(request).floating_ip_pools.list()]


def tenant_floating_ip_get(request, floating_ip_id):
"""
Fetches a floating ip.
Expand Down Expand Up @@ -348,13 +370,13 @@ def security_group_delete(request, security_group_id):
def security_group_rule_create(request, parent_group_id, ip_protocol=None,
from_port=None, to_port=None, cidr=None,
group_id=None):
return SecurityGroup(novaclient(request).\
security_group_rules.create(parent_group_id,
ip_protocol,
from_port,
to_port,
cidr,
group_id))
return SecurityGroupRule(novaclient(request).\
security_group_rules.create(parent_group_id,
ip_protocol,
from_port,
to_port,
cidr,
group_id))


def security_group_rule_delete(request, security_group_rule_id):
Expand Down
Expand Up @@ -81,9 +81,9 @@ def handle(self, request, data):
data['to_port'],
data['cidr'])
messages.success(request, _('Successfully added rule: %s') \
% rule.id)
% unicode(rule))
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in AddRule")
messages.error(request, _('Error adding rule security group: %s')
% e.message)
return shortcuts.redirect(request.build_absolute_uri())
return shortcuts.redirect("horizon:nova:access_and_security:index")
Expand Up @@ -52,6 +52,7 @@ class EditRules(tables.LinkAction):
name = "edit_rules"
verbose_name = _("Edit Rules")
url = "horizon:nova:access_and_security:security_groups:edit_rules"
attrs = {"class": "ajax-modal"}


class SecurityGroupsTable(tables.DataTable):
Expand All @@ -69,32 +70,36 @@ class Meta:


class DeleteRule(tables.DeleteAction):
data_type_singular = _("Security Group Rule")
data_type_plural = _("Security Group Rules")
data_type_singular = _("Rule")
data_type_plural = _("Rules")

def delete(self, request, obj_id):
api.security_group_rule_delete(request, obj_id)

def get_success_url(self, request):
return reverse("horizon:nova:access_and_security:index")


def get_cidr(rule):
return rule.ip_range['cidr']


class RulesTable(tables.DataTable):
protocol = tables.Column("ip_protocol", verbose_name=_("IP Protocol"))
protocol = tables.Column("ip_protocol",
verbose_name=_("IP Protocol"),
filters=(unicode.upper,))
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"))

def sanitize_id(self, obj_id):
return int(obj_id)

def get_object_display(self, datum):
#FIXME (PaulM) Do something prettier here
return ', '.join([':'.join((k, str(v))) for
k, v in datum._apidict.iteritems()])
def get_object_display(self, rule):
return unicode(rule)

class Meta:
name = "rules"
verbose_name = _("Security Group Rules")
table_actions = (DeleteRule,)
row_actions = (DeleteRule,)
Expand Up @@ -24,6 +24,7 @@
from glance.common import exception as glance_exception
from openstackx.api import exceptions as api_exceptions
from novaclient import exceptions as novaclient_exceptions
from novaclient.v1_1 import security_group_rules as nova_rules
from mox import IgnoreArg, IsA

from horizon import api
Expand Down Expand Up @@ -56,12 +57,15 @@ def setUp(self):
sg2.name = 'group_2'

rule = {'id': 1,
'ip_protocol': "tcp",
'ip_protocol': u"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)]
manager = nova_rules.SecurityGroupRuleManager
rule_obj = nova_rules.SecurityGroupRule(manager, rule)
self.rules = [rule_obj]
sg1.rules = self.rules
sg2.rules = self.rules

self.security_groups = (sg1, sg2)
Expand Down Expand Up @@ -179,7 +183,7 @@ def test_edit_rules_add_rule(self):

res = self.client.post(SG_EDIT_RULE_URL, formData)

self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL)
self.assertRedirectsNoFollow(res, INDEX_URL)

def test_edit_rules_add_rule_exception(self):
exception = novaclient_exceptions.ClientException('ClientException',
Expand Down Expand Up @@ -208,7 +212,7 @@ def test_edit_rules_add_rule_exception(self):

res = self.client.post(SG_EDIT_RULE_URL, formData)

self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL)
self.assertRedirectsNoFollow(res, INDEX_URL)

def test_edit_rules_delete_rule(self):
RULE_ID = 1
Expand All @@ -224,7 +228,7 @@ def test_edit_rules_delete_rule(self):
handled = table.maybe_handle()

self.assertEqual(strip_absolute_base(handled['location']),
SG_EDIT_RULE_URL)
INDEX_URL)

def test_edit_rules_delete_rule_exception(self):
RULE_ID = 1
Expand All @@ -244,7 +248,7 @@ def test_edit_rules_delete_rule_exception(self):
handled = table.maybe_handle()

self.assertEqual(strip_absolute_base(handled['location']),
SG_EDIT_RULE_URL)
INDEX_URL)

def test_delete_group(self):
self.mox.StubOutWithMock(api, 'security_group_delete')
Expand Down
Expand Up @@ -73,6 +73,10 @@ def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
context['form'] = form
context['security_group'] = self.object
if request.is_ajax():
context['hide'] = True
self.template_name = ('nova/access_and_security/security_groups'
'/_edit_rules.html')
return self.render_to_response(context)

def post(self, request, *args, **kwargs):
Expand Down
Expand Up @@ -164,7 +164,7 @@ def handle(self, request, data):
LOG.info(msg)
messages.success(request, msg)
return redirect(
'horizon:nova:instances_and_volumes:instances:index')
'horizon:nova:instances_and_volumes:index')

except:
exceptions.handle(request, _('Unable to launch instance.'))
Expand Down
Expand Up @@ -29,7 +29,7 @@
from horizon import test


IMAGES_INDEX_URL = reverse('horizon:nova:images_and_snapshots:images:index')
IMAGES_INDEX_URL = reverse('horizon:nova:images_and_snapshots:index')


class FakeQuota:
Expand Down Expand Up @@ -169,7 +169,7 @@ def test_launch_post(self):
form_data)

self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
reverse('horizon:nova:instances_and_volumes:index'))

def test_launch_flavorlist_error(self):
IMAGE_ID = '1'
Expand Down
Expand Up @@ -49,8 +49,8 @@ def handle(self, request, data):
messages.info(request,
_('Snapshot "%(name)s" created for instance "%(inst)s"') %
{"name": data['name'], "inst": instance.name})
return shortcuts.redirect('horizon:nova:images_and_snapshots'
':snapshots:index')
return shortcuts.redirect('horizon:nova:images_and_snapshots:'
'index')
except api_exceptions.ApiException, e:
msg = _('Error Creating Snapshot: %s') % e.message
LOG.exception(msg)
Expand Down
Expand Up @@ -91,7 +91,7 @@ def test_create_snapshot_get_with_invalid_status(self):
args=[self.bad_server.id]))

self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
reverse('horizon:nova:instances_and_volumes:index'))

def test_create_get_server_exception(self):
self.mox.StubOutWithMock(api, 'server_get')
Expand All @@ -106,7 +106,7 @@ def test_create_get_server_exception(self):
args=[self.good_server.id]))

self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
reverse('horizon:nova:instances_and_volumes:index'))

def test_create_snapshot_post(self):
SNAPSHOT_NAME = 'snappy'
Expand Down Expand Up @@ -136,7 +136,7 @@ def test_create_snapshot_post(self):
formData)

self.assertRedirectsNoFollow(res,
reverse('horizon:nova:images_and_snapshots:snapshots:index'))
reverse('horizon:nova:images_and_snapshots:index'))

def test_create_snapshot_post_exception(self):
SNAPSHOT_NAME = 'snappy'
Expand Down
Expand Up @@ -76,15 +76,15 @@ def create(request, instance_id):
LOG.exception(msg)
messages.error(request, msg)
return shortcuts.redirect(
'horizon:nova:instances_and_volumes:instances:index')
'horizon:nova:instances_and_volumes:index')

valid_states = ['ACTIVE']
if instance.status not in valid_states:
messages.error(request, _("To snapshot, instance state must be\
one of the following: %s") %
', '.join(valid_states))
return shortcuts.redirect(
'horizon:nova:instances_and_volumes:instances:index')
'horizon:nova:instances_and_volumes:index')

return shortcuts.render(request,
'nova/images_and_snapshots/snapshots/create.html',
Expand Down

0 comments on commit 6c35916

Please sign in to comment.