Adds usage vs quota data to the launch instance dialog. Adds a reusable
progress bar indicator. Fixes bug 905563. Change-Id: I5e3cc627be1ac4342f0b4e0a5abb09f70938fa60
This commit is contained in:
parent
9897670519
commit
7ab394829f
@ -41,15 +41,14 @@ VOLUME_STATE_AVAILABLE = "available"
|
||||
|
||||
|
||||
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
|
||||
@ -59,9 +58,8 @@ class Quota(object):
|
||||
|
||||
|
||||
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 = []
|
||||
@ -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',
|
||||
@ -105,7 +104,7 @@ class Server(APIResourceWrapper):
|
||||
|
||||
|
||||
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']
|
||||
@ -147,15 +146,14 @@ class Usage(APIResourceWrapper):
|
||||
|
||||
|
||||
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 \
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -341,8 +330,7 @@ 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)
|
||||
@ -350,8 +338,7 @@ def server_add_floating_ip(request, server, floating_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)
|
||||
@ -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()]
|
||||
|
@ -33,16 +33,15 @@ IMAGES_INDEX_URL = reverse('horizon:nova:images_and_snapshots:index')
|
||||
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)) \
|
||||
@ -119,14 +118,14 @@ class ImageViewTests(test.TestCase):
|
||||
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())
|
||||
@ -144,13 +143,13 @@ class ImageViewTests(test.TestCase):
|
||||
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)
|
||||
|
@ -61,11 +61,8 @@ class LaunchView(forms.ModalFormView):
|
||||
|
||||
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
|
||||
|
@ -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 %}
|
||||
@ -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 %}
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
<div class="progress_bar"><div class="progress_bar_fill" style="width: {% widthratio current_val max_val 100 %}%"></div></div>
|
@ -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):
|
||||
|
@ -156,3 +156,39 @@ class ComputeApiTests(test.APITestCase):
|
||||
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}})
|
||||
|
@ -135,6 +135,7 @@ def data(TEST):
|
||||
TEST.security_group_rules = TestDataContainer()
|
||||
TEST.volumes = TestDataContainer()
|
||||
TEST.quotas = TestDataContainer()
|
||||
TEST.quota_usages = TestDataContainer()
|
||||
TEST.floating_ips = TestDataContainer()
|
||||
TEST.usages = TestDataContainer()
|
||||
TEST.certs = TestDataContainer()
|
||||
@ -152,17 +153,19 @@ def data(TEST):
|
||||
|
||||
# Flavors
|
||||
flavor_1 = flavors.Flavor(flavors.FlavorManager,
|
||||
dict(id="1",
|
||||
name='m1.tiny',
|
||||
vcpus=1,
|
||||
disk=0,
|
||||
ram=512))
|
||||
{'id': "1",
|
||||
'name': 'm1.tiny',
|
||||
'vcpus': 1,
|
||||
'disk': 0,
|
||||
'ram': 512,
|
||||
'OS-FLV-EXT-DATA:ephemeral': 0})
|
||||
flavor_2 = flavors.Flavor(flavors.FlavorManager,
|
||||
dict(id="2",
|
||||
name='m1.massive',
|
||||
vcpus=1000,
|
||||
disk=1024,
|
||||
ram=10000))
|
||||
{'id': "2",
|
||||
'name': 'm1.massive',
|
||||
'vcpus': 1000,
|
||||
'disk': 1024,
|
||||
'ram': 10000,
|
||||
'OS-FLV-EXT-DATA:ephemeral': 2048})
|
||||
TEST.flavors.add(flavor_1, flavor_2)
|
||||
|
||||
# Keypairs
|
||||
@ -203,15 +206,29 @@ def data(TEST):
|
||||
quota_data = dict(metadata_items='1',
|
||||
injected_file_content_bytes='1',
|
||||
volumes='1',
|
||||
gigabytes='1',
|
||||
ram=1,
|
||||
gigabytes='1000',
|
||||
ram=10000,
|
||||
floating_ips='1',
|
||||
instances='1',
|
||||
instances='10',
|
||||
injected_files='1',
|
||||
cores='1')
|
||||
cores='10')
|
||||
quota = quotas.QuotaSet(quotas.QuotaSetManager, quota_data)
|
||||
TEST.quotas.add(quota)
|
||||
|
||||
# Quota Usages
|
||||
TEST.quota_usages.add({'gigabytes': {'available': 1000,
|
||||
'used': 0,
|
||||
'quota': 1000},
|
||||
'instances': {'available': 10,
|
||||
'used': 0,
|
||||
'quota': 10},
|
||||
'ram': {'available': 10000,
|
||||
'used': 0,
|
||||
'quota': 10000},
|
||||
'cores': {'available': 20,
|
||||
'used': 0,
|
||||
'quota': 20}})
|
||||
|
||||
# Servers
|
||||
vals = {"host": "http://nova.example.com:8774",
|
||||
"name": "server_1",
|
||||
|
@ -852,3 +852,36 @@ form .error {
|
||||
color: #b94a48;
|
||||
border: 1px solid #E9B1B0;
|
||||
}
|
||||
|
||||
.progress_bar {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 1px solid #CCC;
|
||||
background-color: #CCC;
|
||||
}
|
||||
|
||||
.progress_bar_fill {
|
||||
height: 100%;
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
.quota_title {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.quota_title strong {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.quota_title strong span {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.quota_title p {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.quota_bar {
|
||||
height: 15px;
|
||||
margin: -8px 0 8px;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ set -o errexit
|
||||
# Increment me any time the environment should be rebuilt.
|
||||
# This includes dependncy changes, directory renames, etc.
|
||||
# Simple integer secuence: 1, 2, 3...
|
||||
environment_version=11
|
||||
environment_version=12
|
||||
#--------------------------------------------------------#
|
||||
|
||||
function usage {
|
||||
|
Loading…
x
Reference in New Issue
Block a user