From e35016314e5d76aea7ff4c1c8c4b2d16dbe629cd Mon Sep 17 00:00:00 2001 From: Tzu-Mainn Chen Date: Mon, 14 Jul 2014 20:03:30 +0200 Subject: [PATCH] Update node index view to match Juno wireframes For now, instead of having a single 'Registered' tab with a graphical/ table switch, we have two separate tabs. The former is under 'Overview'; the latter under 'Registered'. The graphical portion of the 'Overview' is left for a future patch. 'Discovered' tab is eliminated pending a better understanding of the Ironic API that enables it. Additional styling to come in future patches. Partial-Implements: blueprint node-index-view-update Change-Id: Id62493ab7dfb8cee97905c59d457e8b1bdc72ecf --- tuskar_ui/infrastructure/nodes/tables.py | 83 +++++-------------- tuskar_ui/infrastructure/nodes/tabs.py | 52 +++++------- .../nodes/templates/nodes/_overview.html | 62 ++++---------- .../nodes/templates/nodes/index.html | 2 +- tuskar_ui/infrastructure/nodes/tests.py | 70 +++------------- tuskar_ui/infrastructure/nodes/views.py | 20 ----- tuskar_ui/infrastructure/overcloud/tables.py | 2 +- tuskar_ui/infrastructure/overcloud/views.py | 2 + tuskar_ui/test/api_tests/node_tests.py | 2 +- tuskar_ui/test/test_data/heat_data.py | 12 ++- tuskar_ui/test/test_data/node_data.py | 37 ++++++++- 11 files changed, 118 insertions(+), 226 deletions(-) diff --git a/tuskar_ui/infrastructure/nodes/tables.py b/tuskar_ui/infrastructure/nodes/tables.py index 2236c9bc0..e68d9e4f3 100644 --- a/tuskar_ui/infrastructure/nodes/tables.py +++ b/tuskar_ui/infrastructure/nodes/tables.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from django.template import defaultfilters +from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from horizon import tables @@ -50,24 +50,27 @@ class NodeFilterAction(tables.FilterAction): return filter(comp, nodes) -class NodesTable(tables.DataTable): - node = tables.Column(lambda node: node.driver_info['ipmi_address'], +def get_role_link(datum): + # TODO(tzumainn): this could probably be done more efficiently + # by getting the resource for all nodes at once + if datum.role_id: + return reverse('horizon:infrastructure:overcloud:role', + kwargs={'stack_id': datum.stack_id, + 'role_id': datum.role_id}) + + +class RegisteredNodesTable(tables.DataTable): + node = tables.Column('uuid', link="horizon:infrastructure:nodes:detail", - verbose_name=_("Node")) - - # TODO(lsmola) waits for Ironic - # architecture = tables.Column( - # lambda node: "", - # verbose_name=_("Architecture")) - - cpu = tables.Column(lambda node: node.properties['cpu'], - verbose_name=_("CPU (cores)")) - ram = tables.Column(lambda node: defaultfilters.filesizeformat( - node.properties['ram']), - verbose_name=_("RAM")) - local_disk = tables.Column(lambda node: defaultfilters.filesizeformat( - node.properties['local_disk']), - verbose_name=_("Local Disk")) + verbose_name=_("Node Name")) + instance_ip = tables.Column(lambda n: + n.instance.public_ip if n.instance else '-', + verbose_name=_("Instance IP")) + provisioning_status = tables.Column('provisioning_status', + verbose_name=_("Provisioned")) + role_name = tables.Column('role_name', + link=get_role_link, + verbose_name=_("Deployment Role")) power_state = tables.Column("power_state", verbose_name=_("Power"), status=True, @@ -80,51 +83,11 @@ class NodesTable(tables.DataTable): class Meta: name = "nodes_table" verbose_name = _("Nodes") - table_actions = () - row_actions = () + table_actions = (NodeFilterAction,) + row_actions = (DeleteNode,) def get_object_id(self, datum): return datum.uuid def get_object_display(self, datum): return datum.uuid - - -class FreeNodesTable(NodesTable): - - # TODO(jtomasek): waits for Ironic to expose IP - # node = tables.Column(lambda node: node.driver_info['ipmi_address'], - # link="horizon:infrastructure:nodes:detail", - # verbose_name=_("Node")) - node = tables.Column("uuid", - link="horizon:infrastructure:nodes:detail", - verbose_name=_("Node")) - - class Meta: - name = "free_nodes" - verbose_name = _("Free Nodes") - table_actions = (DeleteNode, - NodeFilterAction,) - row_actions = (DeleteNode,) - - -class DeployedNodesTable(NodesTable): - - deployment_role = tables.Column("role_name", - verbose_name=_("Deployment Role")) - - # TODO(lsmola) waits for Ceilometer baremetal metrics - # capacity = tables.Column( - # lambda node: "", - # verbose_name=_("Capacity")) - - health = tables.Column('instance_status', - verbose_name=_("Health")) - - class Meta: - name = "deployed_nodes" - verbose_name = _("Deployed Nodes") - table_actions = (NodeFilterAction,) - row_actions = () - columns = ('node', 'deployment_role', 'capacity', 'architecture', - 'cpu', 'ram', 'local_disk', 'health', 'power_state') diff --git a/tuskar_ui/infrastructure/nodes/tabs.py b/tuskar_ui/infrastructure/nodes/tabs.py index 7b45a5061..21d852232 100644 --- a/tuskar_ui/infrastructure/nodes/tabs.py +++ b/tuskar_ui/infrastructure/nodes/tabs.py @@ -28,6 +28,10 @@ class OverviewTab(tabs.Tab): template_name = "infrastructure/nodes/_overview.html" def get_context_data(self, request): + nodes = api.node.Node.list(request) + cpus = sum(int(node.properties['cpu']) for node in nodes) + ram = sum(int(node.properties['ram']) for node in nodes) + local_disk = sum(int(node.properties['local_disk']) for node in nodes) deployed_nodes = api.node.Node.list(request, associated=True) free_nodes = api.node.Node.list(request, associated=False) deployed_nodes_error = api.node.filter_nodes( @@ -38,6 +42,9 @@ class OverviewTab(tabs.Tab): total_nodes_healthy = api.node.filter_nodes(total_nodes, healthy=True) return { + 'cpus': cpus, + 'ram_gb': ram / 1024.0 ** 3, + 'local_disk_gb': local_disk / 1024.0 ** 3, 'total_nodes_healthy': total_nodes_healthy, 'total_nodes_error': total_nodes_error, 'deployed_nodes': deployed_nodes, @@ -47,57 +54,38 @@ class OverviewTab(tabs.Tab): } -class DeployedTab(tabs.TableTab): - table_classes = (tables.DeployedNodesTable,) - name = _("Deployed") - slug = "deployed" +class RegisteredTab(tabs.TableTab): + table_classes = (tables.RegisteredNodesTable,) + name = _("Registered") + slug = "registered" template_name = "horizon/common/_detail_table.html" def get_items_count(self): - return len(self.get_deployed_nodes_data()) + return len(self.get_nodes_table_data()) - def get_deployed_nodes_data(self): + def get_nodes_table_data(self): redirect = urlresolvers.reverse('horizon:infrastructure:nodes:index') - deployed_nodes = api.node.Node.list(self.request, associated=True, - _error_redirect=redirect) + nodes = api.node.Node.list(self.request, _error_redirect=redirect) if 'errors' in self.request.GET: - return api.node.filter_nodes(deployed_nodes, healthy=False) + return api.node.filter_nodes(nodes, healthy=False) - for node in deployed_nodes: + for node in nodes: # TODO(tzumainn): this could probably be done more efficiently # by getting the resource for all nodes at once try: resource = api.heat.Resource.get_by_node(self.request, node) node.role_name = resource.role.name + node.role_id = resource.role.id + node.stack_id = resource.stack.id except exceptions.NotFound: node.role_name = '-' - return deployed_nodes - - -class FreeTab(tabs.TableTab): - table_classes = (tables.FreeNodesTable,) - name = _("Free") - slug = "free" - template_name = "horizon/common/_detail_table.html" - - def get_items_count(self): - return len(self.get_free_nodes_data()) - - def get_free_nodes_data(self): - redirect = urlresolvers.reverse('horizon:infrastructure:nodes:index') - free_nodes = api.node.Node.list(self.request, associated=False, - _error_redirect=redirect) - - if 'errors' in self.request.GET: - return api.node.filter_nodes(free_nodes, healthy=False) - - return free_nodes + return nodes class NodeTabs(tabs.TabGroup): slug = "nodes" - tabs = (OverviewTab, DeployedTab, FreeTab) + tabs = (OverviewTab, RegisteredTab) sticky = True template_name = "horizon/common/_items_count_tab_group.html" diff --git a/tuskar_ui/infrastructure/nodes/templates/nodes/_overview.html b/tuskar_ui/infrastructure/nodes/templates/nodes/_overview.html index c21f8dac3..0dd8a41ff 100644 --- a/tuskar_ui/infrastructure/nodes/templates/nodes/_overview.html +++ b/tuskar_ui/infrastructure/nodes/templates/nodes/_overview.html @@ -5,64 +5,32 @@
-

{% trans 'Health Status' %}

+

{% trans 'Hardware Inventory' %}

- - + + + + + + + + + diff --git a/tuskar_ui/infrastructure/nodes/templates/nodes/index.html b/tuskar_ui/infrastructure/nodes/templates/nodes/index.html index 880fa3620..5b3a75d11 100644 --- a/tuskar_ui/infrastructure/nodes/templates/nodes/index.html +++ b/tuskar_ui/infrastructure/nodes/templates/nodes/index.html @@ -1,7 +1,7 @@ {% extends 'infrastructure/base.html' %} {% load i18n %} {% load url from future %} -{% block title %}{% trans 'Registered Nodes' %}{% endblock %} +{% block title %}{% trans 'Nodes' %}{% endblock %} {% block page_header %} {% include 'horizon/common/_items_count_domain_page_header.html' with title=_('Registered Nodes') items_count=nodes_count %} diff --git a/tuskar_ui/infrastructure/nodes/tests.py b/tuskar_ui/infrastructure/nodes/tests.py index dfc03cd3d..f79b02573 100644 --- a/tuskar_ui/infrastructure/nodes/tests.py +++ b/tuskar_ui/infrastructure/nodes/tests.py @@ -52,15 +52,15 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase): }) as mock: res = self.client.get(INDEX_URL) # FIXME(lsmola) optimize, this should call 1 time, what the hell - self.assertEqual(mock.list.call_count, 8) + self.assertEqual(mock.list.call_count, 5) self.assertTemplateUsed( res, 'infrastructure/nodes/index.html') self.assertTemplateUsed(res, 'infrastructure/nodes/_overview.html') - def test_free_nodes(self): - free_nodes = [api.node.Node(node) - for node in self.ironicclient_nodes.list()] + def test_registered_nodes(self): + registered_nodes = [api.node.Node(node) + for node in self.ironicclient_nodes.list()] roles = [api.tuskar.OvercloudRole(r) for r in TEST_DATA.tuskarclient_roles.list()] instance = TEST_DATA.novaclient_servers.first() @@ -73,7 +73,7 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase): }), patch('tuskar_ui.api.node.Node', **{ 'spec_set': ['list'], - 'list.return_value': free_nodes, + 'list.return_value': registered_nodes, }), patch('tuskar_ui.api.node.nova', **{ 'spec_set': ['server_get'], @@ -84,71 +84,25 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase): 'image_get.return_value': image, }), ) as (_OvercloudRole, Node, _nova, _glance): - res = self.client.get(INDEX_URL + '?tab=nodes__free') + res = self.client.get(INDEX_URL + '?tab=nodes__registered') # FIXME(lsmola) horrible count, optimize - self.assertEqual(Node.list.call_count, 10) - - self.assertTemplateUsed(res, - 'infrastructure/nodes/index.html') - self.assertTemplateUsed(res, 'horizon/common/_detail_table.html') - self.assertItemsEqual(res.context['free_nodes_table'].data, - free_nodes) - - def test_free_nodes_list_exception(self): - with patch('tuskar_ui.api.node.Node', **{ - 'spec_set': ['list'], - 'list.side_effect': self._raise_tuskar_exception, - }) as mock: - res = self.client.get(INDEX_URL + '?tab=nodes__free') - self.assertEqual(mock.list.call_count, 3) - - self.assertRedirectsNoFollow(res, INDEX_URL) - - def test_deployed_nodes(self): - deployed_nodes = [api.node.Node(node) - for node in self.ironicclient_nodes.list()] - roles = [api.tuskar.OvercloudRole(r) - for r in TEST_DATA.tuskarclient_roles.list()] - instance = TEST_DATA.novaclient_servers.first() - image = TEST_DATA.glanceclient_images.first() - - with contextlib.nested( - patch('tuskar_ui.api.tuskar.OvercloudRole', **{ - 'spec_set': ['list', 'name'], - 'list.return_value': roles, - }), - patch('tuskar_ui.api.node.Node', **{ - 'spec_set': ['list'], - 'list.return_value': deployed_nodes, - }), - patch('tuskar_ui.api.node.nova', **{ - 'spec_set': ['server_get'], - 'server_get.return_value': instance, - }), - patch('tuskar_ui.api.node.glance', **{ - 'spec_set': ['image_get'], - 'image_get.return_value': image, - }), - ) as (_OvercloudRole, Node, _nova, _glance): - res = self.client.get(INDEX_URL + '?tab=nodes__deployed') - # FIXME(lsmola) horrible count, optimize - self.assertEqual(Node.list.call_count, 10) + self.assertEqual(Node.list.call_count, 6) self.assertTemplateUsed( res, 'infrastructure/nodes/index.html') self.assertTemplateUsed(res, 'horizon/common/_detail_table.html') - self.assertItemsEqual(res.context['deployed_nodes_table'].data, - deployed_nodes) + self.assertItemsEqual(res.context['nodes_table_table'].data, + registered_nodes) - def test_deployed_nodes_list_exception(self): + def test_registered_nodes_list_exception(self): instance = TEST_DATA.novaclient_servers.first() with patch('tuskar_ui.api.node.Node', **{ 'spec_set': ['list', 'instance'], 'instance': instance, 'list.side_effect': self._raise_tuskar_exception, }) as mock: - res = self.client.get(INDEX_URL + '?tab=nodes__deployed') - self.assertEqual(mock.list.call_count, 3) + res = self.client.get(INDEX_URL + '?tab=nodes__registered') + self.assertEqual(mock.list.call_count, 4) self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/tuskar_ui/infrastructure/nodes/views.py b/tuskar_ui/infrastructure/nodes/views.py index ab6eb5f5e..efdb36212 100644 --- a/tuskar_ui/infrastructure/nodes/views.py +++ b/tuskar_ui/infrastructure/nodes/views.py @@ -35,26 +35,6 @@ class IndexView(horizon_tabs.TabbedTableView): tab_group_class = tabs.NodeTabs template_name = 'infrastructure/nodes/index.html' - def get_free_nodes_count(self): - free_nodes_count = len(api.node.Node.list( - self.request, associated=False)) - return free_nodes_count - - def get_deployed_nodes_count(self): - deployed_nodes_count = len(api.node.Node.list(self.request, - associated=True)) - return deployed_nodes_count - - def get_context_data(self, **kwargs): - context = super(IndexView, self).get_context_data(**kwargs) - - context['free_nodes_count'] = self.get_free_nodes_count() - context['deployed_nodes_count'] = self.get_deployed_nodes_count() - context['nodes_count'] = (context['free_nodes_count'] + - context['deployed_nodes_count']) - - return context - class RegisterView(horizon_forms.ModalFormView): form_class = forms.NodeFormset diff --git a/tuskar_ui/infrastructure/overcloud/tables.py b/tuskar_ui/infrastructure/overcloud/tables.py index c40682434..dd7a0bf15 100644 --- a/tuskar_ui/infrastructure/overcloud/tables.py +++ b/tuskar_ui/infrastructure/overcloud/tables.py @@ -19,7 +19,7 @@ from horizon import tables from tuskar_ui.infrastructure.nodes import tables as nodes_tables -class OvercloudRoleNodeTable(nodes_tables.DeployedNodesTable): +class OvercloudRoleNodeTable(nodes_tables.RegisteredNodesTable): class Meta: name = "overcloud_role__nodetable" diff --git a/tuskar_ui/infrastructure/overcloud/views.py b/tuskar_ui/infrastructure/overcloud/views.py index 68bfc1079..fc5ce103c 100644 --- a/tuskar_ui/infrastructure/overcloud/views.py +++ b/tuskar_ui/infrastructure/overcloud/views.py @@ -171,6 +171,8 @@ class OvercloudRoleView(horizon_tables.DataTableView, try: resource = api.heat.Resource.get_by_node(self.request, node) node.role_name = resource.role.name + node.role_id = resource.role.id + node.stack_id = resource.stack.id except horizon_exceptions.NotFound: node.role_name = '-' diff --git a/tuskar_ui/test/api_tests/node_tests.py b/tuskar_ui/test/api_tests/node_tests.py index f3050007b..f2c144c37 100644 --- a/tuskar_ui/test/api_tests/node_tests.py +++ b/tuskar_ui/test/api_tests/node_tests.py @@ -85,7 +85,7 @@ class NodeAPITests(test.APITestCase): for node in ret_val: self.assertIsInstance(node, api.node.Node) - self.assertEqual(5, len(ret_val)) + self.assertEqual(6, len(ret_val)) def test_node_delete(self): node = self.baremetalclient_nodes.first() diff --git a/tuskar_ui/test/test_data/heat_data.py b/tuskar_ui/test/test_data/heat_data.py index 900a068b8..a879b43c0 100644 --- a/tuskar_ui/test/test_data/heat_data.py +++ b/tuskar_ui/test/test_data/heat_data.py @@ -162,7 +162,8 @@ def data(TEST): 'flavor': { 'id': '1', }, - 'status': 'ACTIVE'}) + 'status': 'ACTIVE', + 'public_ip': '192.168.1.1'}) s_2 = servers.Server( servers.ServerManager(None), {'id': 'bb', @@ -172,7 +173,8 @@ def data(TEST): 'flavor': { 'id': '2', }, - 'status': 'ACTIVE'}) + 'status': 'ACTIVE', + 'public_ip': '192.168.1.2'}) s_3 = servers.Server( servers.ServerManager(None), {'id': 'cc', @@ -182,7 +184,8 @@ def data(TEST): 'flavor': { 'id': '1', }, - 'status': 'BUILD'}) + 'status': 'BUILD', + 'public_ip': '192.168.1.3'}) s_4 = servers.Server( servers.ServerManager(None), {'id': 'dd', @@ -192,7 +195,8 @@ def data(TEST): 'flavor': { 'id': '1', }, - 'status': 'ERROR'}) + 'status': 'ERROR', + 'public_ip': '192.168.1.4'}) TEST.novaclient_servers.add(s_1, s_2, s_3, s_4) # Image diff --git a/tuskar_ui/test/test_data/node_data.py b/tuskar_ui/test/test_data/node_data.py index e87ec3ec8..2a0fcfb4c 100644 --- a/tuskar_ui/test/test_data/node_data.py +++ b/tuskar_ui/test/test_data/node_data.py @@ -92,8 +92,22 @@ def data(TEST): "pm_user": None, "interfaces": [{"address": "52:54:00:90:38:01"}], }) + bm_node_6 = baremetal.BareMetalNode( + baremetal.BareMetalNodeManager(None), + {'id': '6', + 'uuid': 'ff-66', + 'instance_uuid': None, + "service_host": "undercloud", + "cpus": 1, + "memory_mb": 4096, + "local_gb": 20, + 'task_state': None, + "pm_address": None, + "pm_user": None, + "interfaces": [{"address": "52:54:00:90:38:01"}], + }) TEST.baremetalclient_nodes.add( - bm_node_1, bm_node_2, bm_node_3, bm_node_4, bm_node_5) + bm_node_1, bm_node_2, bm_node_3, bm_node_4, bm_node_5, bm_node_6) # IronicNode TEST.ironicclient_nodes = test_data_utils.TestDataContainer() @@ -192,7 +206,26 @@ def data(TEST): }, 'power_state': 'error', }) - TEST.ironicclient_nodes.add(node_1, node_2, node_3, node_4, node_5) + node_6 = node.Node( + node.NodeManager(None), + {'id': '6', + 'uuid': 'ff-66', + 'instance_uuid': None, + 'driver': 'pxe_ipmitool', + 'driver_info': { + 'ipmi_address': '5.5.5.5', + 'ipmi_username': 'admin', + 'ipmi_password': 'password', + 'ip_address': '1.2.2.6' + }, + '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, node_6) # Ports TEST.ironicclient_ports = test_data_utils.TestDataContainer()
- - {% trans 'Deployed Nodes' %} - ({{ deployed_nodes|length|default:0 }}) - - - {% if deployed_nodes_error %} - - {% if deployed_nodes_error|length == 1 %} - {% url 'horizon:infrastructure:nodes:detail' deployed_nodes_error.0.uuid as node_detail_url %} - {% else %} - {% url 'horizon:infrastructure:nodes:index' as nodes_index_url %} - {% endif %} - - {% blocktrans count error_count=deployed_nodes_error|length %} - {{ error_count }} node with erros - {% plural %} - {{ error_count }} nodes with errors - {% endblocktrans %} - {% else %} - - {% trans 'All nodes are performing correctly' %} - {% endif %} - {{ cpus }} {% trans 'CPU cores' %}
{{ ram_gb }} {% trans 'GB of memory' %}
{{ local_disk_gb }} {% trans 'GB of storage' %}
- + {% trans 'Free Nodes' %} ({{ free_nodes|length|default:0 }})
- {% if free_nodes_error %} - - {% if free_nodes_error|length == 1 %} - {% comment %} - Replace id with uuid when ironicclient is used instead baremetalclient - {% endcomment %} - {% url 'horizon:infrastructure:nodes:detail' free_nodes_error.0.id as node_detail_url %} - {% else %} - {% url 'horizon:infrastructure:nodes:index' as nodes_index_url %} - {% endif %} - - {% blocktrans count error_count=free_nodes_error|length %} - {{ error_count }} node with errors - {% plural %} - {{ error_count }} nodes with errors - {% endblocktrans %} - {% else %} - - {% trans 'All nodes are performing correctly' %} - {% endif %} + + {% trans 'Provisioned Nodes' %} + ({{ deployed_nodes|length|default:0 }}) +