diff --git a/tuskar_ui/api.py b/tuskar_ui/api.py index 2404c7b74..f123e07e0 100644 --- a/tuskar_ui/api.py +++ b/tuskar_ui/api.py @@ -40,8 +40,10 @@ def tuskarclient(request): return c -class Overcloud(base.APIResourceWrapper): - _attrs = ('id', 'stack_name', 'stack_status') +# TODO(Tzu-Mainn Chen): change this to APIResourceWrapper once +# ResourceCategory object exists in tuskar +class Overcloud(base.APIDictWrapper): + _attrs = ('id', 'stack_id', 'name', 'description') @classmethod def create(cls, request, overcloud_sizing): @@ -64,43 +66,42 @@ class Overcloud(base.APIResourceWrapper): # overcloud = tuskarclient(request).overclouds.create( # 'overcloud', # overcloud_sizing) - overcloud = test_data().heatclient_stacks.first() + overcloud = test_data().tuskarclient_overclouds.first() return cls(overcloud) @classmethod - def get(cls, request): - # Assumptions: - # * hard-coded stack name ('overcloud') + def list(cls, request): + # Return: + # * a list of Overclouds in Tuskar + + # TODO(Tzu-Mainn Chen): remove test data when possible + # ocs = tuskarclient(request).overclouds.list() + ocs = test_data().tuskarclient_overclouds.list() + + return [cls(oc) for oc in ocs] + + @classmethod + def get(cls, request, overcloud_id): + # Required: + # * overcloud_id # Return: # * the 'overcloud' stack object # TODO(Tzu-Mainn Chen): remove test data when possible - # overcloud = heatclient(request).stacks.get('overcloud') - overcloud = test_data().heatclient_stacks.first() + # overcloud = tuskarclient(request).overclouds.get(overcloud_id) + overcloud = test_data().tuskarclient_overclouds.first() return cls(overcloud) @cached_property - def resources(self): - # Assumptions: - # * hard-coded stack name ('overcloud') + def stack(self, request): # Return: - # * a list of Resources associated with the Overcloud + # * the Heat stack associated with this overcoud # TODO(Tzu-Mainn Chen): remove test data when possible - # resources = heatclient(request).resources.list(self.id) - resources = test_data().heatclient_resources.list - - return [Resource(r) for r in resources] - - @cached_property - def nodes(self): - # Assumptions: - # * hard-coded stack name ('overcloud') - # Return: - # * a list of Nodes indirectly associated with the Overcloud - - return [resource.node for resource in self.resources] + # stack = heatclient(request).stacks.get(self.stack_id) + stack = test_data().heatclient_stacks.first() + return stack @cached_property def is_deployed(self): @@ -256,20 +257,22 @@ class Resource(base.APIResourceWrapper): if self.physical_resource_id == n.instance_uuid), None) - @cached_property - def resource_category(self): - # Questions: - # * is a resource_type mapped directly to a ResourceCategory? - # * can we assume that the resource_type equals the category - # name? + +# TODO(Tzu-Mainn Chen): change this to APIResourceWrapper once +# ResourceCategory object exists in tuskar +class ResourceCategory(base.APIDictWrapper): + _attrs = ('id', 'name', 'description', 'image_id') + + @classmethod + def list(cls, request): # Return: - # * the ResourceCategory matching this resource + # * a list of Resource Categories in Tuskar. - return ResourceCategory({'name': self.resource_type}) + # TODO(Tzu-Mainn Chen): remove test data when possible + # categories = tuskarclient(request).resource_categories.list() - -class ResourceCategory(base.APIResourceWrapper): - _attrs = ('name') + rcs = test_data().tuskarclient_resource_categories.list() + return [cls(rc) for rc in rcs] @cached_property def image(self): @@ -284,15 +287,37 @@ class ResourceCategory(base.APIResourceWrapper): return "image_name" - @cached_property def resources(self, overcloud): - # Questions: - # * can we assume that the resource_type equals the - # category name? # Required: # * overcloud # Return: # * the resources within the stack that match the # resource category - return [r for r in overcloud.resources if r.resource_type == self.name] + # 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 diff --git a/tuskar_ui/infrastructure/overcloud/tabs.py b/tuskar_ui/infrastructure/overcloud/tabs.py new file mode 100644 index 000000000..71910c2c9 --- /dev/null +++ b/tuskar_ui/infrastructure/overcloud/tabs.py @@ -0,0 +1,77 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Nebula, Inc. +# +# 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 exceptions +from horizon import tabs + +from tuskar_ui import api + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = ("infrastructure/overcloud/_detail_overview.html") + + def get_context_data(self, request): + context = {} + overcloud = self.tab_group.kwargs['overcloud'] + + try: + categories = api.ResourceCategory.list(request) + except Exception: + categories = {} + exceptions.handle(request, + _('Unable to retrieve resource categories.')) + + context['categories'] = [] + for category in categories: + context['categories'].append( + self._get_category_data(overcloud, category)) + + # also get expected instance counts + return context + + def _get_category_data(self, overcloud, category): + instances = category.instances(overcloud) + category.instance_count = len(instances) + if category.instance_count > 0: + category.running_instance_count = len( + [i for i in instances if i.status == 'ACTIVE']) + category.error_instance_count = len( + [i for i in instances if i.status == 'ERROR']) + category.other_instance_count = category.instance_count - \ + (category.running_instance_count + + category.error_instance_count) + category.running_instance_percentage = 100 * \ + category.running_instance_count / category.instance_count + return category + + +class ConfigurationTab(tabs.Tab): + name = _("Configuration") + slug = "configuration" + template_name = ("infrastructure/overcloud/_detail_configuration.html") + + def get_context_data(self, request): + return {} + + +class DetailTabs(tabs.TabGroup): + slug = "detail" + tabs = (OverviewTab, ConfigurationTab) + sticky = True diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_configuration.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_configuration.html new file mode 100644 index 000000000..d5b3c89a1 --- /dev/null +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_configuration.html @@ -0,0 +1,5 @@ +{% load i18n %} +{% load url from future%} + +
+
diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html new file mode 100644 index 000000000..1fc57a2f7 --- /dev/null +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html @@ -0,0 +1,28 @@ +{% load i18n %} +{% load url from future%} + +
+
+

{% trans "Resource Categories" %}

+
+ {% for category in categories %} +

{{ category.instance_count }} {{ category.name }}

+ {% if category.error_instance_count > 0 %} + {{ category.error_instance_count }} {% trans "instances are down" %} +
+ {% endif %} + {% if category.other_instance_count > 0 %} + {{ category.other_instance_count }} {% trans "instances are building" %} +
+ {% endif %} + {% if category.running_instance_percentage %} + {{ category.running_instance_percentage }} {% trans "% of instances are running" %} +
+ {% endif %} +
+ {% endfor %} +
+
+

{% trans "Resource Categories Distribution" %}

+
+
diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html new file mode 100644 index 000000000..b10ffeb3a --- /dev/null +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html @@ -0,0 +1,16 @@ +{% extends 'infrastructure/base.html' %} +{% load i18n %} +{% load url from future %} +{% block title %}{% trans 'My Openstack Deployment' %}{% endblock %} + +{% block page_header %} + {% include 'horizon/common/_domain_page_header.html' with title=_('My Openstack Deployment') %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/tuskar_ui/infrastructure/overcloud/tests.py b/tuskar_ui/infrastructure/overcloud/tests.py index 9c2899aa3..eef9bf51c 100644 --- a/tuskar_ui/infrastructure/overcloud/tests.py +++ b/tuskar_ui/infrastructure/overcloud/tests.py @@ -34,28 +34,30 @@ tuskar_data.data(TEST_DATA) class OvercloudTests(test.BaseAdminViewTests): def test_index_overcloud_undeployed(self): - stack = api.Overcloud(TEST_DATA.heatclient_stacks.first) + oc = api.Overcloud(TEST_DATA.tuskarclient_overclouds.first()) with patch('tuskar_ui.api.Overcloud', **{ 'spec_set': ['get', 'is_deployed'], 'is_deployed': False, - 'get.return_value': stack, + 'get.return_value': oc, }) as Overcloud: res = self.client.get(INDEX_URL) request = Overcloud.get.call_args_list[0][0][0] # This is a hack. - self.assertListEqual(Overcloud.get.call_args_list, [call(request)]) + self.assertListEqual(Overcloud.get.call_args_list, + [call(request, 1)]) self.assertRedirectsNoFollow(res, CREATE_URL) def test_index_overcloud_deployed(self): - stack = api.Overcloud(TEST_DATA.heatclient_stacks.first) + oc = api.Overcloud(TEST_DATA.tuskarclient_overclouds.first()) with patch('tuskar_ui.api.Overcloud', **{ 'spec_set': ['get', 'is_deployed'], 'is_deployed': True, - 'get.return_value': stack, + 'get.return_value': oc, }) as Overcloud: res = self.client.get(INDEX_URL) request = Overcloud.get.call_args_list[0][0][0] # This is a hack. - self.assertListEqual(Overcloud.get.call_args_list, [call(request)]) + self.assertListEqual(Overcloud.get.call_args_list, + [call(request, 1)]) self.assertRedirectsNoFollow(res, DETAIL_URL) diff --git a/tuskar_ui/infrastructure/overcloud/views.py b/tuskar_ui/infrastructure/overcloud/views.py index 91e176ef7..ada54f02d 100644 --- a/tuskar_ui/infrastructure/overcloud/views.py +++ b/tuskar_ui/infrastructure/overcloud/views.py @@ -13,12 +13,15 @@ # under the License. from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ from django.views.generic import base as base_views +from horizon import exceptions +from horizon import tabs as horizon_tabs import horizon.workflows from tuskar_ui import api -from tuskar_ui.infrastructure.overcloud.workflows import deployed +from tuskar_ui.infrastructure.overcloud import tabs from tuskar_ui.infrastructure.overcloud.workflows import undeployed @@ -26,7 +29,7 @@ class IndexView(base_views.RedirectView): permanent = False def get_redirect_url(self): - overcloud = api.Overcloud.get(self.request) + overcloud = api.Overcloud.get(self.request, 1) if overcloud is not None and overcloud.is_deployed: redirect = reverse('horizon:infrastructure:overcloud:detail', args=(overcloud.id,)) @@ -40,6 +43,19 @@ class CreateView(horizon.workflows.WorkflowView): template_name = 'infrastructure/_fullscreen_workflow_base.html' -class DetailView(horizon.workflows.WorkflowView): - workflow_class = deployed.Workflow - template_name = 'infrastructure/_fullscreen_workflow_base.html' +class DetailView(horizon_tabs.TabView): + tab_group_class = tabs.DetailTabs + template_name = 'infrastructure/overcloud/detail.html' + + def get_data(self, request, **kwargs): + overcloud_id = kwargs['overcloud_id'] + try: + return api.Overcloud.get(request, overcloud_id) + except Exception: + msg = _("Unable to retrieve deployment.") + redirect = reverse('horizon:infrastructure:overcloud:index') + exceptions.handle(request, msg, redirect=redirect) + + def get_tabs(self, request, **kwargs): + overcloud = self.get_data(request, **kwargs) + return self.tab_group_class(request, overcloud=overcloud, **kwargs) diff --git a/tuskar_ui/infrastructure/overcloud/workflows/deployed.py b/tuskar_ui/infrastructure/overcloud/workflows/deployed.py deleted file mode 100644 index ada0c9855..000000000 --- a/tuskar_ui/infrastructure/overcloud/workflows/deployed.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- 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 _ -import horizon.workflows - -from tuskar_ui.infrastructure.overcloud.workflows import deployed_configuration -from tuskar_ui.infrastructure.overcloud.workflows import deployed_overview - - -class Workflow(horizon.workflows.Workflow): - slug = 'deployed_overcloud' - name = _("My Openstack Deployment") - default_steps = ( - deployed_overview.Step, - deployed_configuration.Step, - ) - finalize_button_name = _("Scale Deployment") - # TODO(rdopierqalski) Point this to the scaling forms. - success_url = 'horizon:infrastructure:overcloud:index' - - def handle(self, request, context): - pass diff --git a/tuskar_ui/infrastructure/overcloud/workflows/deployed_configuration.py b/tuskar_ui/infrastructure/overcloud/workflows/deployed_configuration.py deleted file mode 100644 index 2ea9d24fa..000000000 --- a/tuskar_ui/infrastructure/overcloud/workflows/deployed_configuration.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- 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 _ -import horizon.workflows - - -class Action(horizon.workflows.Action): - class Meta: - slug = 'deployed_configuration' - name = _("Configuration") - - -class Step(horizon.workflows.Step): - action_class = Action - contributes = () diff --git a/tuskar_ui/infrastructure/overcloud/workflows/deployed_overview.py b/tuskar_ui/infrastructure/overcloud/workflows/deployed_overview.py deleted file mode 100644 index f12355c74..000000000 --- a/tuskar_ui/infrastructure/overcloud/workflows/deployed_overview.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- 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 _ -import horizon.workflows - - -class Action(horizon.workflows.Action): - class Meta: - slug = 'deployed_overview' - name = _("Overview") - - -class Step(horizon.workflows.Step): - action_class = Action - contributes = () diff --git a/tuskar_ui/test/test_data/tuskar_data.py b/tuskar_ui/test/test_data/tuskar_data.py index 92ede8ecd..47ae8e679 100644 --- a/tuskar_ui/test/test_data/tuskar_data.py +++ b/tuskar_ui/test/test_data/tuskar_data.py @@ -16,6 +16,7 @@ from heatclient.v1 import resources from heatclient.v1 import stacks from ironicclient.v1 import node from ironicclient.v1 import port +from novaclient.v1_1 import servers def data(TEST): @@ -24,7 +25,7 @@ def data(TEST): TEST.heatclient_stacks = test_data_utils.TestDataContainer() stack_1 = stacks.Stack( stacks.StackManager(None), - {'id': '1', + {'id': 'stack-id-1', 'stack_name': 'overcloud', 'stack_status': 'RUNNING'}) TEST.heatclient_stacks.add(stack_1) @@ -79,7 +80,39 @@ def data(TEST): 'local_disk': '1', }, 'power_state': 'rebooting'}) - TEST.ironicclient_nodes.add(node_1, node_2, node_3) + node_4 = node.Node( + node.NodeManager(None), + {'uuid': 'cc-44', + 'instance_uuid': 'cc', + 'driver': 'pxe_ipmitool', + 'driver_info': { + 'ipmi_address': '4.4.4.4', + 'ipmi_username': 'admin', + 'ipmi_password': 'password', + }, + 'properties': { + 'cpu': '8', + 'ram': '16', + 'local_disk': '10', + }, + 'power_state': 'on'}) + node_5 = node.Node( + node.NodeManager(None), + {'uuid': 'dd-55', + 'instance_uuid': 'dd', + 'driver': 'pxe_ipmitool', + 'driver_info': { + 'ipmi_address': '5.5.5.5', + 'ipmi_username': 'admin', + 'ipmi_password': 'password', + }, + 'properties': { + 'cpu': '8', + 'ram': '16', + 'local_disk': '10', + }, + 'power_state': 'on'}) + TEST.ironicclient_nodes.add(node_1, node_2, node_3, node_4, node_5) # Ports TEST.ironicclient_ports = test_data_utils.TestDataContainer() @@ -109,7 +142,8 @@ def data(TEST): TEST.heatclient_resources = test_data_utils.TestDataContainer() resource_1 = resources.Resource( resources.ResourceManager(None), - {'stack_id': '1', + {'id': '1-resource-id', + 'stack_id': 'stack-id-1', 'resource_name': 'Compute', 'logical_resource_id': 'Compute', 'physical_resource_id': 'aa', @@ -117,12 +151,92 @@ def data(TEST): 'resource_type': 'AWS::EC2::Instance'}) resource_2 = resources.Resource( resources.ResourceManager(None), - {'stack_id': '1', - 'resource_name': 'Control', - 'logical_resource_id': 'Control', + {'id': '2-resource-id', + 'stack_id': 'stack-id-1', + 'resource_name': 'Controller', + 'logical_resource_id': 'Controller', 'physical_resource_id': 'bb', 'resource_status': 'CREATE_COMPLETE', 'resource_type': 'AWS::EC2::Instance'}) - TEST.heatclient_resources.add(resource_1, resource_2) + resource_3 = resources.Resource( + resources.ResourceManager(None), + {'id': '3-resource-id', + 'stack_id': 'stack-id-1', + 'resource_name': 'Compute', + 'logical_resource_id': 'Compute', + 'physical_resource_id': 'cc', + 'resource_status': 'CREATE_COMPLETE', + 'resource_type': 'AWS::EC2::Instance'}) + resource_4 = resources.Resource( + resources.ResourceManager(None), + {'id': '4-resource-id', + 'stack_id': 'stack-id-4', + 'resource_name': 'Compute', + 'logical_resource_id': 'Compute', + 'physical_resource_id': 'dd', + 'resource_status': 'CREATE_COMPLETE', + 'resource_type': 'AWS::EC2::Instance'}) + TEST.heatclient_resources.add(resource_1, + resource_2, + resource_3, + resource_4) + + # Server + TEST.novaclient_servers = test_data_utils.TestDataContainer() + s_1 = servers.Server( + servers.ServerManager(None), + {'id': 'aa', + 'name': 'Compute', + 'image': 'compute-image', + 'status': 'ACTIVE'}) + s_2 = servers.Server( + servers.ServerManager(None), + {'id': 'bb', + 'name': 'Controller', + 'image': 'controller-image', + 'status': 'ACTIVE'}) + s_3 = servers.Server( + servers.ServerManager(None), + {'id': 'cc', + 'name': 'Compute', + 'image': 'compute-image', + 'status': 'BUILD'}) + s_4 = servers.Server( + servers.ServerManager(None), + {'id': 'dd', + 'name': 'Compute', + 'image': 'compute-image', + 'status': 'ERROR'}) + TEST.novaclient_servers.add(s_1, s_2, s_3, s_4) + + # Overcloud + TEST.tuskarclient_overclouds = test_data_utils.TestDataContainer() + # TODO(Tzu-Mainn Chen): fix these to create Tuskar Overcloud objects + # once the api supports it + oc_1 = {'id': 1, + 'stack_id': 'stack-id-1', + 'name': 'overcloud', + 'description': 'overcloud'} + TEST.tuskarclient_overclouds.add(oc_1) # ResourceCategory + TEST.tuskarclient_resource_categories = test_data_utils.TestDataContainer() + # TODO(Tzu-Mainn Chen): fix these to create Tuskar ResourceCategory objects + # once the api supports it + rc_1 = {'id': 1, + 'name': 'Controller', + 'description': 'controller resource category', + 'image_id': 'image-id-1'} + rc_2 = {'id': 2, + 'name': 'Compute', + 'description': 'compute resource category', + 'image_id': 'image-id-2'} + rc_3 = {'id': 3, + 'name': 'Object Storage', + 'description': 'object storage resource category', + 'image_id': 'image-id-3'} + rc_4 = {'id': 4, + 'name': 'Block Storage', + 'description': 'block storage resource category', + 'image_id': 'image-id-4'} + TEST.tuskarclient_resource_categories.add(rc_1, rc_2, rc_3, rc_4)