Skip to content

Commit

Permalink
Adds usage vs quota data to the launch instance dialog. Adds a reusable
Browse files Browse the repository at this point in the history
progress bar indicator.

Fixes bug 905563.

Change-Id: I5e3cc627be1ac4342f0b4e0a5abb09f70938fa60
  • Loading branch information
treshenry committed Feb 29, 2012
1 parent 9897670 commit 7ab3948
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 91 deletions.
72 changes: 42 additions & 30 deletions horizon/horizon/api/nova.py
Expand Up @@ -41,15 +41,14 @@


class VNCConsole(APIDictWrapper):
"""
Wrapper for the "console" dictionary returned by the
"""Wrapper for the "console" dictionary returned by the
novaclient.servers.get_vnc_console method.
"""
_attrs = ['url', 'type']


class Quota(object):
""" Wrapper for individual limits in a quota. """
"""Wrapper for individual limits in a quota."""
def __init__(self, name, limit):
self.name = name
self.limit = limit
Expand All @@ -59,9 +58,8 @@ def __repr__(self):


class QuotaSet(object):
"""
Wrapper for novaclient.quotas.QuotaSet objects which wraps the individual
quotas inside Quota objects.
"""Wrapper for novaclient.quotas.QuotaSet objects which wraps the
individual quotas inside Quota objects.
"""
def __init__(self, apiresource):
self.items = []
Expand All @@ -78,6 +76,7 @@ class Server(APIResourceWrapper):
"""Simple wrapper around novaclient.server.Server
Preserves the request info so image name can later be retrieved
"""
_attrs = ['addresses', 'attrs', 'id', 'image', 'links',
'metadata', 'name', 'private_ip', 'public_ip', 'status', 'uuid',
Expand Down Expand Up @@ -105,7 +104,7 @@ def reboot(self, hardness=REBOOT_HARD):


class Usage(APIResourceWrapper):
"""Simple wrapper around contrib/simple_usage.py"""
"""Simple wrapper around contrib/simple_usage.py."""
_attrs = ['start', 'server_usages', 'stop', 'tenant_id',
'total_local_gb_usage', 'total_memory_mb_usage',
'total_vcpus_usage', 'total_hours']
Expand Down Expand Up @@ -147,15 +146,14 @@ def disk_gb_hours(self):


class SecurityGroup(APIResourceWrapper):
"""
Wrapper around novaclient.security_groups.SecurityGroup which wraps its
"""Wrapper around novaclient.security_groups.SecurityGroup which wraps its
rules in SecurityGroupRule objects and allows access to them.
"""
_attrs = ['id', 'name', 'description', 'tenant_id']

@property
def rules(self):
""" Wraps transmitted rule info in the novaclient rule class. """
"""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 \
Expand Down Expand Up @@ -226,38 +224,29 @@ def flavor_list(request):


def tenant_floating_ip_list(request):
"""
Fetches a list of all floating ips.
"""
"""Fetches a list of all floating ips."""
return novaclient(request).floating_ips.list()


def floating_ip_pools_list(request):
"""
Fetches a list of all floating ip pools.
"""
"""Fetches a list of all floating ip pools."""
return novaclient(request).floating_ip_pools.list()


def tenant_floating_ip_get(request, floating_ip_id):
"""
Fetches a floating ip.
"""
"""Fetches a floating ip."""
return novaclient(request).floating_ips.get(floating_ip_id)


def tenant_floating_ip_allocate(request, pool=None):
"""
Allocates a floating ip to tenant.
Optionally you may provide a pool for which you would like the IP.
"""Allocates a floating ip to tenant. Optionally you may provide a pool
for which you would like the IP.
"""
return novaclient(request).floating_ips.create(pool=pool)


def tenant_floating_ip_release(request, floating_ip_id):
"""
Releases floating ip from the pool of a tenant.
"""
"""Releases floating ip from the pool of a tenant."""
return novaclient(request).floating_ips.delete(floating_ip_id)


Expand Down Expand Up @@ -310,7 +299,7 @@ def server_list(request, search_opts=None, all_tenants=False):


def server_console_output(request, instance_id, tail_length=None):
"""Gets console output of an instance"""
"""Gets console output of an instance."""
return novaclient(request).servers.get_console_output(instance_id,
length=tail_length)

Expand Down Expand Up @@ -341,17 +330,15 @@ def server_update(request, instance_id, name):


def server_add_floating_ip(request, server, floating_ip):
"""
Associates floating IP to server's fixed IP.
"""Associates floating IP to server's fixed IP.
"""
server = novaclient(request).servers.get(server)
fip = novaclient(request).floating_ips.get(floating_ip)
return novaclient(request).servers.add_floating_ip(server.id, fip.ip)


def server_remove_floating_ip(request, server, floating_ip):
"""
Removes relationship between floating and server's fixed ip.
"""Removes relationship between floating and server's fixed ip.
"""
fip = novaclient(request).floating_ips.get(floating_ip)
server = novaclient(request).servers.get(fip.instance_id)
Expand All @@ -378,6 +365,31 @@ def usage_list(request, start, end):
return [Usage(u) for u in novaclient(request).usage.list(start, end, True)]


def tenant_quota_usages(request):
"""Builds a dictionary of current usage against quota for the current
tenant.
"""
# TODO(tres): Make this capture floating_ips and volumes as well.
instances = server_list(request)
quotas = tenant_quota_get(request, request.user.tenant_id)
flavors = dict([(f.id, f) for f in flavor_list(request)])
usages = {'instances': {'flavor_fields': [], 'used': len(instances)},
'cores': {'flavor_fields': ['vcpus'], 'used': 0},
'gigabytes': {'flavor_fields': ['disk', 'ephemeral'], 'used': 0},
'ram': {'flavor_fields': ['ram'], 'used': 0}}

for usage in usages:
for instance in instances:
for flavor_field in usages[usage]['flavor_fields']:
usages[usage]['used'] += getattr(
flavors[instance.flavor['id']], flavor_field)
usages[usage]['quota'] = getattr(quotas, usage)
usages[usage]['available'] = usages[usage]['quota'] - \
usages[usage]['used']

return usages


def security_group_list(request):
return [SecurityGroup(g) for g in novaclient(request).\
security_groups.list()]
Expand Down
Expand Up @@ -33,16 +33,15 @@
class ImageViewTests(test.TestCase):
def test_launch_get(self):
image = self.images.first()
tenant = self.tenants.first()
quota = self.quotas.first()
quota_usages = self.quota_usages.first()

self.mox.StubOutWithMock(api, 'image_get_meta')
self.mox.StubOutWithMock(api, 'tenant_quota_get')
self.mox.StubOutWithMock(api, 'tenant_quota_usages')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'keypair_list')
self.mox.StubOutWithMock(api, 'security_group_list')
api.image_get_meta(IsA(http.HttpRequest), image.id).AndReturn(image)
api.tenant_quota_get(IsA(http.HttpRequest), tenant.id).AndReturn(quota)
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_usages)
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs.list())
api.security_group_list(IsA(http.HttpRequest)) \
Expand Down Expand Up @@ -119,14 +118,14 @@ def test_launch_flavorlist_error(self):
image = self.images.first()

self.mox.StubOutWithMock(api, 'image_get_meta')
self.mox.StubOutWithMock(api, 'tenant_quota_get')
self.mox.StubOutWithMock(api, 'tenant_quota_usages')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'keypair_list')
self.mox.StubOutWithMock(api, 'security_group_list')
api.image_get_meta(IsA(http.HttpRequest),
image.id).AndReturn(image)
api.tenant_quota_get(IsA(http.HttpRequest),
self.tenant.id).AndReturn(self.quotas.first())
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(
self.quota_usages.first())
exc = keystone_exceptions.ClientException('Failed.')
api.flavor_list(IsA(http.HttpRequest)).AndRaise(exc)
api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs.list())
Expand All @@ -144,13 +143,13 @@ def test_launch_keypairlist_error(self):
image = self.images.first()

self.mox.StubOutWithMock(api, 'image_get_meta')
self.mox.StubOutWithMock(api, 'tenant_quota_get')
self.mox.StubOutWithMock(api, 'tenant_quota_usages')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'keypair_list')
self.mox.StubOutWithMock(api, 'security_group_list')
api.image_get_meta(IsA(http.HttpRequest), image.id).AndReturn(image)
api.tenant_quota_get(IsA(http.HttpRequest),
self.tenant.id).AndReturn(self.quotas.first())
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(
self.quota_usages.first())
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
exception = keystone_exceptions.ClientException('Failed.')
api.keypair_list(IsA(http.HttpRequest)).AndRaise(exception)
Expand Down
Expand Up @@ -61,11 +61,8 @@ def get_object(self, *args, **kwargs):

def get_context_data(self, **kwargs):
context = super(LaunchView, self).get_context_data(**kwargs)
tenant_id = self.request.user.tenant_id
try:
quotas = api.tenant_quota_get(self.request, tenant_id)
quotas.ram = int(quotas.ram)
context['quotas'] = quotas
context['usages'] = api.tenant_quota_usages(self.request)
except:
exceptions.handle(self.request)
return context
Expand Down
@@ -1,5 +1,6 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}

{% load horizon i18n %}

{% block form_id %}launch_image_form{% endblock %}
{% block form_action %}{% url horizon:nova:images_and_snapshots:images:launch image.id %}{% endblock %}
Expand All @@ -9,39 +10,42 @@

{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Specify the details for launching an instance. Also please make note of the table below; all projects have quotas which define the limit of resources they are allowed to provision." %}</p>
<table class="table table-striped table-bordered">
<tr>
<th>{% trans "Quota Name" %}</th>
<th>{% trans "Limit" %}</th>
</tr>
<tr>
<td>{% trans "RAM (MB)" %}</td>
<td>{{ quotas.ram }}MB</td>
</tr>
<tr>
<td>{% trans "Floating IPs" %}</td>
<td>{{ quotas.floating_ips }}</td>
</tr>
<tr>
<td>{% trans "Instances" %}</td>
<td>{{ quotas.instances }}</td>
</tr>
<tr>
<td>{% trans "Volumes" %}</td>
<td>{{ quotas.volumes }}</td>
</tr>
<tr>
<td>{% trans "Available Disk" %}</td>
<td>{{ quotas.gigabytes }}GB</td>
</tr>
</table>
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Specify the details for launching an instance. The chart below shows the resources used by this project in relation to the project's quotas." %}</p>
<h3>{% trans "Project Quotas" %}</h3>

<div class="quota_title">
<strong>{% trans "Instance Count" %} <span>({{ usages.instances.used }})</span></strong>
<p>{{ usages.instances.available }} {% trans "Available" %}</p>
</div>
<div class="clearfix"></div>
<div class="quota_bar">{% horizon_progress_bar usages.instances.used usages.instances.quota %}</div>

<div class="quota_title">
<strong>{% trans "VCPUs" %} <span>({{ usages.cores.used }})</span></strong>
<p>{{ usages.cores.available }} {% trans "Available" %}</p>
</div>
<div class="clearfix"></div>
<div class="quota_bar">{% horizon_progress_bar usages.cores.used usages.cores.quota %}</div>

<div class="quota_title">
<strong>{% trans "Disk" %} <span>({{ usages.gigabytes.used }} {% trans "GB" %})</span></strong>
<p>{{ usages.gigabytes.available }} {% trans "GB" %} {% trans "Available" %}</p>
</div>
<div class="clearfix"></div>
<div class="quota_bar">{% horizon_progress_bar usages.gigabytes.used usages.gigabytes.quota %}</div>

<div class="quota_title">
<strong>{% trans "Memory" %} <span>({{ usages.ram.used }} {% trans "MB" %})</span></strong>
<p>{{ usages.ram.available }} {% trans "MB" %} {% trans "Available" %}</p>
</div>
<div class="clearfix"></div>
<div class="quota_bar">{% horizon_progress_bar usages.ram.used usages.ram.quota %}</div>
</div>
{% endblock %}

Expand Down
@@ -0,0 +1 @@
<div class="progress_bar"><div class="progress_bar_fill" style="width: {% widthratio current_val max_val 100 %}%"></div></div>
18 changes: 18 additions & 0 deletions horizon/horizon/templatetags/horizon.py
Expand Up @@ -96,6 +96,24 @@ def horizon_dashboard_nav(context):
'current': context['request'].horizon['panel'].slug}


@register.inclusion_tag('horizon/common/_progress_bar.html')
def horizon_progress_bar(current_val, max_val):
""" Renders a progress bar based on parameters passed to the tag. The first
parameter is the current value and the second is the max value.
Example: ``{% progress_bar 25 50 %}``
This will generate a half-full progress bar.
The rendered progress bar will fill the area of its container. To constrain
the rendered size of the bar provide a container with appropriate width and
height styles.
"""
return {'current_val': current_val,
'max_val': max_val}


class JSTemplateNode(template.Node):
""" Helper node for the ``jstemplate`` template tag. """
def __init__(self, nodelist):
Expand Down
36 changes: 36 additions & 0 deletions horizon/horizon/tests/api_tests/nova_tests.py
Expand Up @@ -156,3 +156,39 @@ def test_server_add_floating_ip(self):
server.id,
floating_ip.id)
self.assertIsInstance(server, api.nova.Server)

def test_tenant_quota_usages(self):
servers = self.servers.list()
flavors = self.flavors.list()
quotas = self.quotas.first()
novaclient = self.stub_novaclient()

novaclient.servers = self.mox.CreateMockAnything()
novaclient.servers.list(True, {'project_id': '1'}).AndReturn(servers)
novaclient.flavors = self.mox.CreateMockAnything()
novaclient.flavors.list().AndReturn(flavors)
novaclient.quotas = self.mox.CreateMockAnything()
novaclient.quotas.get(self.tenant.id).AndReturn(quotas)
self.mox.ReplayAll()

quota_usages = api.tenant_quota_usages(self.request)

self.assertIsInstance(quota_usages, dict)
self.assertEquals(quota_usages,
{'gigabytes': {'available': 1000,
'used': 0,
'flavor_fields': ['disk',
'ephemeral'],
'quota': 1000},
'instances': {'available': 8,
'used': 2,
'flavor_fields': [],
'quota': 10},
'ram': {'available': 8976,
'used': 1024,
'flavor_fields': ['ram'],
'quota': 10000},
'cores': {'available': 8,
'used': 2,
'flavor_fields': ['vcpus'],
'quota': 10}})

0 comments on commit 7ab3948

Please sign in to comment.