Add unstyled overcloud resource category page

API calls are used, although mock data is returned.
The page will need further styling to match wireframes.

The ResourceCategory methods to fetch instances/resources
are moved under Overcloud instead, as it feels more
appropriately scoped there.

Change-Id: I9df98000ab40c089ec0d2ffb7e654d0e29ef2545
This commit is contained in:
Tzu-Mainn Chen 2014-01-17 22:20:09 -05:00
parent 9422ced4aa
commit 440cd5652a
8 changed files with 309 additions and 50 deletions

View File

@ -16,6 +16,8 @@ import logging
import django.conf
from horizon.utils import memoized
from openstack_dashboard.api import base
from openstack_dashboard.test.test_data import utils
from tuskar_ui.cached_property import cached_property # noqa
@ -40,6 +42,16 @@ def tuskarclient(request):
return c
def list_to_dict(object_list, key_attribute='id'):
# Required:
# * object_list
# Optional:
# * key_attribute
# Return:
# * a dict of the objects indexed by key_attribute
return dict((getattr(o, key_attribute), o) for o in object_list)
# TODO(Tzu-Mainn Chen): change this to APIResourceWrapper once
# ResourceCategory object exists in tuskar
class Overcloud(base.APIDictWrapper):
@ -91,6 +103,7 @@ class Overcloud(base.APIDictWrapper):
# TODO(Tzu-Mainn Chen): remove test data when possible
# overcloud = tuskarclient(request).overclouds.get(overcloud_id)
overcloud = test_data().tuskarclient_overclouds.first()
return cls(overcloud)
@cached_property
@ -113,6 +126,95 @@ class Overcloud(base.APIDictWrapper):
# TODO(rdopieralski) Actually implement it
return False
@memoized.memoized
def resources(self, resource_category, with_joins=False):
# Required:
# * resource_category
# Return:
# * the resources within the overcloud that match the
# resource category
# TODO(Tzu-Mainn Chen): uncomment when possible
#resources = tuskarclient(request).overclouds.get_resources(
# self.id, resource_category.id)
resources = [r for r in test_data().heatclient_resources.list()
if r.logical_resource_id == resource_category.name]
if not with_joins:
return [Resource(r) for r in resources]
instances_dict = list_to_dict(Instance.list(None, with_joins=True))
nodes_dict = list_to_dict(Node.list(None, associated=True),
key_attribute='instance_uuid')
joined_resources = []
for r in resources:
instance = instances_dict.get(r.physical_resource_id, None)
node = nodes_dict.get(r.physical_resource_id, None)
joined_resources.append(Resource(r,
instance=instance,
node=node))
return joined_resources
@memoized.memoized
def instances(self, resource_category):
# Required:
# * resource_category
# Return:
# * the instances that match the resource category
resources = self.resources(resource_category, with_joins=True)
return [r.instance for r in resources]
class Instance(base.APIResourceWrapper):
_attrs = ('id', 'name', 'image', 'status')
def __init__(self, apiresource, **kwargs):
super(Instance, self).__init__(apiresource)
if 'node' in kwargs:
self._node = kwargs['node']
@classmethod
def get(cls, request, instance_id):
# Required:
# * instance_id
# Return:
# * the Server associated with the instace_id
# TODO(Tzu-Mainn Chen): remove test data when possible
# instance = novaclient(request).servers.get(instance_id)
servers = test_data().novaclient_servers.list()
server = next((s for s in servers if instance_id == s.id),
None)
return cls(server)
@classmethod
def list(cls, request, with_joins=False):
# Return:
# * a list of Servers registered in Nova.
# TODO(Tzu-Mainn Chen): remove test data when possible
# servers = novaclient(request).servers.list(detailed=True)
servers = test_data().novaclient_servers.list()
if not with_joins:
return [cls(s) for s in servers]
nodes_dict = list_to_dict(Node.list(None, associated=True),
key_attribute='instance_uuid')
joined_servers = []
for s in servers:
node = nodes_dict.get(s.id, None)
joined_servers.append(Instance(s, node=node))
return joined_servers
@cached_property
def node(self):
if hasattr(self, '_node'):
return self._node
return Node.get_by_instance_uuid(None, self.id)
class Node(base.APIResourceWrapper):
_attrs = ('uuid', 'instance_uuid', 'driver', 'driver_info',
@ -161,14 +263,32 @@ class Node(base.APIResourceWrapper):
# TODO(Tzu-Mainn Chen): remove test data when possible
# node = ironicclient(request).nodes.get(uuid)
node = test_data().ironicclient_nodes.first()
nodes = test_data().ironicclient_nodes.list()
node = next((n for n in nodes if uuid == n.uuid),
None)
return cls(node)
@classmethod
def get_by_instance_uuid(cls, request, instance_uuid):
# Required:
# * instance_uuid
# Return:
# * the Node associated with the instance_uuid
# TODO(Tzu-Mainn Chen): remove test data when possible
#node = ironicclient(request).nodes.get_by_instance_uuid(
# instance_uuid)
nodes = test_data().ironicclient_nodes.list()
node = next((n for n in nodes if instance_uuid == n.instance_uuid),
None)
return cls(node)
@classmethod
def list(cls, request, associated=None):
# Optional:
# * free
# * associated
# Return:
# * a list of Nodes registered in Ironic.
@ -229,6 +349,13 @@ class Resource(base.APIResourceWrapper):
_attrs = ('resource_name', 'resource_type', 'resource_status',
'physical_resource_id')
def __init__(self, apiresource, **kwargs):
super(Resource, self).__init__(apiresource)
if 'instance' in kwargs:
self._instance = kwargs['instance']
if 'node' in kwargs:
self._node = kwargs['node']
@classmethod
def get(cls, request, overcloud, resource_name):
# Required:
@ -248,14 +375,21 @@ class Resource(base.APIResourceWrapper):
return cls(resource)
@cached_property
def instance(self):
# Return:
# * return resource's associated instance
if hasattr(self, '_instance'):
return self._instance
return Instance.get(None, self.physical_resource_id)
@cached_property
def node(self):
# Return:
# * return resource's associated Node
return next((n for n in Node.list
if self.physical_resource_id == n.instance_uuid),
None)
if hasattr(self, '_node'):
return self._node
return Node.get_by_instance_uuid(self.physical_resource_id)
# TODO(Tzu-Mainn Chen): change this to APIResourceWrapper once
@ -270,54 +404,33 @@ class ResourceCategory(base.APIDictWrapper):
# TODO(Tzu-Mainn Chen): remove test data when possible
# categories = tuskarclient(request).resource_categories.list()
rcs = test_data().tuskarclient_resource_categories.list()
return [cls(rc) for rc in rcs]
@classmethod
def get(cls, request, category_id):
# Required:
# * category_id
# Return:
# * the 'resource_category' stack object
# TODO(Tzu-Mainn Chen): remove test data when possible
# category = tuskarclient(request).resource_categories.get(category_id)
categories = ResourceCategory.list(request)
category = next((c for c in categories if category_id == str(c.id)),
None)
return cls(category)
@cached_property
def image(self):
# Questions:
# * when a user uploads an image, how do we enforce
# that it matches the image name?
# Return:
# * the image name associated with the ResourceCategory
# TODO(Tzu-Mainn Chen): uncomment when possible
# return some-api-call-to-tuskarclient
# TODO(Tzu-Mainn Chen): remove test data when possible
# image = glanceclient(request).images.get(self.image_id)
images = test_data().glanceclient_images.list()
image = next((i for i in images if self.image_id == i.id),
None)
return "image_name"
def resources(self, overcloud):
# Required:
# * overcloud
# Return:
# * the resources within the stack that match the
# resource category
# TODO(Tzu-Mainn Chen): uncomment when possible
#resources = tuskarclient(request).overclouds.get_resources(
# overcloud.id, self.id)
return [r for r in test_data().heatclient_resources.list()
if r.logical_resource_id == self.name]
def instances(self, overcloud):
# Required:
# * overcloud
# Return:
# * the instances corresponding to the resources within the
# stack that match the resource category
#resources = tuskarclient(request).overclouds.get_resources(
# overcloud.id, self.id)
# TODO(Tzu-Mainn Chen): uncomment real api calls and remove test
# data when possible
instances = []
all_instances = test_data().novaclient_servers.list()
for r in self.resources(overcloud):
#instance = novaclient(request).servers.get(r.physical_resource_id)
instance = next((i for i in all_instances
if i.id == r.physical_resource_id),
None)
instances.append(instance)
return instances
return image

View File

@ -0,0 +1,46 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class ResourceCategoryInstanceTable(tables.DataTable):
instance_name = tables.Column("name",
verbose_name=_("Instance Name"))
instance_status = tables.Column("status",
verbose_name=_("Instance Status"))
node_uuid = tables.Column(
transform=lambda i: i.node.uuid,
verbose_name=_("Node UUID"))
node_cpu = tables.Column(
transform=lambda i: i.node.properties['cpu'],
verbose_name=_("Node CPU"))
node_ram = tables.Column(
transform=lambda i: i.node.properties['ram'],
verbose_name=_("Node RAM (GB)"))
node_local_disk = tables.Column(
transform=lambda i: i.node.properties['local_disk'],
verbose_name=_("Node Local Disk (TB)"))
node_power_state = tables.Column(
transform=lambda i: i.node.power_state,
verbose_name=_("Power State"))
class Meta:
name = "resource_category__instancetable"
verbose_name = _("Instances")
table_actions = ()
row_actions = ()

View File

@ -38,6 +38,7 @@ class OverviewTab(tabs.Tab):
exceptions.handle(request,
_('Unable to retrieve resource categories.'))
context['overcloud'] = overcloud
context['categories'] = []
for category in categories:
context['categories'].append(
@ -47,7 +48,7 @@ class OverviewTab(tabs.Tab):
return context
def _get_category_data(self, overcloud, category):
instances = category.instances(overcloud)
instances = overcloud.instances(category)
category.instance_count = len(instances)
if category.instance_count > 0:
category.running_instance_count = len(

View File

@ -6,7 +6,7 @@
<h3>{% trans "Resource Categories" %}</h3>
<hr />
{% for category in categories %}
<h4>{{ category.instance_count }} {{ category.name }}</h4>
<h4><a href="{% url 'horizon:infrastructure:overcloud:resource_category' overcloud.id category.id%}">{{ category.instance_count }} {{ category.name }}</a></h4>
{% if category.error_instance_count > 0 %}
{{ category.error_instance_count }} {% trans "instances are down" %}
<br />

View File

@ -0,0 +1,21 @@
{% extends 'infrastructure/base.html' %}
{% load i18n %}
{% load url from future %}
{% block title %}{% trans 'Resource Category' %}: {{ category.name }}{% endblock %}
{% block page_header %}
{% include 'horizon/common/_domain_page_header.html' with title=_('Resource Category: ')|add:category.name %}
{% endblock page_header %}
{% block main %}
<div class="row-fluid">
<div class="span12">
<hr />
<h4>{{ instances|length }} {% trans 'instances' %}</h4>
<br />
<h4>{{ image.name }}</h4>
{{ table.render }}
</div>
</div>
{% endblock %}

View File

@ -24,4 +24,8 @@ urlpatterns = defaults.patterns(
name='create'),
defaults.url(r'^(?P<overcloud_id>[^/]+)/$',
views.DetailView.as_view(), name='detail'),
defaults.url(r'^(?P<overcloud_id>[^/]+)/resource_category/'
'(?P<category_id>[^/]+)$',
views.ResourceCategoryView.as_view(),
name='resource_category'),
)

View File

@ -17,10 +17,13 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import base as base_views
from horizon import exceptions
from horizon import tables as horizon_tables
from horizon import tabs as horizon_tabs
from horizon.utils import memoized
import horizon.workflows
from tuskar_ui import api
from tuskar_ui.infrastructure.overcloud import tables
from tuskar_ui.infrastructure.overcloud import tabs
from tuskar_ui.infrastructure.overcloud.workflows import undeployed
@ -59,3 +62,53 @@ class DetailView(horizon_tabs.TabView):
def get_tabs(self, request, **kwargs):
overcloud = self.get_data(request, **kwargs)
return self.tab_group_class(request, overcloud=overcloud, **kwargs)
class ResourceCategoryView(horizon_tables.DataTableView):
table_class = tables.ResourceCategoryInstanceTable
template_name = 'infrastructure/overcloud/resource_category.html'
def get_data(self):
overcloud = self._get_overcloud()
category = self._get_category(overcloud)
return overcloud.instances(category)
def get_context_data(self, **kwargs):
context = super(ResourceCategoryView, self).get_context_data(**kwargs)
overcloud = self._get_overcloud()
category = self._get_category(overcloud)
context['category'] = category
context['image'] = category.image
context['instances'] = overcloud.instances(category)
return context
@memoized.memoized
def _get_overcloud(self):
overcloud_id = self.kwargs['overcloud_id']
try:
overcloud = api.Overcloud.get(self.request, overcloud_id)
except Exception:
msg = _("Unable to retrieve deployment.")
redirect = reverse('horizon:infrastructure:overcloud:index')
exceptions.handle(self.request, msg, redirect=redirect)
return overcloud
@memoized.memoized
def _get_category(self, overcloud):
category_id = self.kwargs['category_id']
try:
category = api.ResourceCategory.get(self.request, category_id)
except Exception:
msg = _("Unable to retrieve resource category.")
redirect = reverse('horizon:infrastructure:overcloud:detail',
args=(overcloud.id,))
exceptions.handle(self.request, msg, redirect=redirect)
return category

View File

@ -12,6 +12,7 @@
from openstack_dashboard.test.test_data import utils as test_data_utils
from glanceclient.v1 import images
from heatclient.v1 import resources
from heatclient.v1 import stacks
from ironicclient.v1 import node
@ -240,3 +241,23 @@ def data(TEST):
'description': 'block storage resource category',
'image_id': 'image-id-4'}
TEST.tuskarclient_resource_categories.add(rc_1, rc_2, rc_3, rc_4)
# Image
TEST.glanceclient_images = test_data_utils.TestDataContainer()
image_1 = images.Image(
images.ImageManager(None),
{'id': 'image-id-1',
'name': 'Controller Image'})
image_2 = images.Image(
images.ImageManager(None),
{'id': 'image-id-2',
'name': 'Compute Image'})
image_3 = images.Image(
images.ImageManager(None),
{'id': 'image-id-3',
'name': 'Object Storage Image'})
image_4 = images.Image(
images.ImageManager(None),
{'id': 'image-id-4',
'name': 'Block Storage Image'})
TEST.glanceclient_images.add(image_1, image_2, image_3, image_4)