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:
parent
9422ced4aa
commit
440cd5652a
209
tuskar_ui/api.py
209
tuskar_ui/api.py
@ -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
|
||||
|
46
tuskar_ui/infrastructure/overcloud/tables.py
Normal file
46
tuskar_ui/infrastructure/overcloud/tables.py
Normal 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 = ()
|
@ -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(
|
||||
|
@ -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 />
|
||||
|
@ -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 %}
|
@ -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'),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user