Re-factor nodes index tables

Tabs are now: 'All', 'Provisioned', 'Free', 'Maintenance'.

Change-Id: Ia8d26c6aa4b4d7dc9ac78f25aa6ab9477b2f0781
This commit is contained in:
Tzu-Mainn Chen 2014-10-24 19:50:02 +00:00
parent e67223b693
commit 8a77093c01
8 changed files with 269 additions and 131 deletions

View File

@ -28,9 +28,18 @@ from tuskar_ui.handle_errors import handle_errors # noqa
from tuskar_ui.utils import utils
# power states
ERROR_STATES = set(['deploy failed', 'error'])
POWER_ON_STATES = set(['on', 'power on'])
# overall state of the node; not power states
DISCOVERING_STATE = 'discovering'
DISCOVERED_STATE = 'discovered'
PROVISIONED_STATE = 'provisioned'
PROVISIONING_FAILED_STATE = 'provisioning failed'
PROVISIONING_STATE = 'provisioning'
FREE_STATE = 'free'
LOG = logging.getLogger(__name__)
@ -67,7 +76,7 @@ def image_get(request, image_id):
class IronicNode(base.APIResourceWrapper):
_attrs = ('id', 'uuid', 'instance_uuid', 'driver', 'driver_info',
'properties', 'power_state', 'target_power_state',
'maintenance')
'provision_state', 'maintenance', 'extra')
def __init__(self, apiresource, request=None):
super(IronicNode, self).__init__(apiresource)
@ -286,6 +295,21 @@ class IronicNode(base.APIResourceWrapper):
def cpu_arch(self):
return self.properties.get('cpu_arch', None)
@cached_property
def state(self):
if self.maintenance:
if self.extra.get('on_discovery', 'false') == 'true':
return DISCOVERING_STATE
return DISCOVERED_STATE
else:
if self.instance_uuid:
if self.provision_state == 'active':
return PROVISIONED_STATE
if self.provision_state in ('deploy failed', 'error'):
return PROVISIONING_FAILED_STATE
return PROVISIONING_STATE
return FREE_STATE
class BareMetalNode(base.APIResourceWrapper):
_attrs = ('id', 'uuid', 'instance_uuid', 'memory_mb', 'cpus', 'local_gb',
@ -421,6 +445,10 @@ class BareMetalNode(base.APIResourceWrapper):
}
return task_state_dict.get(self.task_state, 'off')
@cached_property
def state(self):
return self.power_state
@cached_property
def target_power_state(self):
return None
@ -477,7 +505,7 @@ class NodeClient(object):
class Node(base.APIResourceWrapper):
_attrs = ('id', 'uuid', 'instance_uuid', 'driver', 'driver_info',
_attrs = ('id', 'uuid', 'instance_uuid', 'driver', 'driver_info', 'state',
'power_state', 'target_power_state', 'addresses', 'maintenance',
'cpus', 'memory_mb', 'local_gb', 'cpu_arch')

View File

@ -136,24 +136,41 @@ def get_power_state_with_transition(node):
return node.power_state
class RegisteredNodesTable(tables.DataTable):
def get_state_string(node):
state_dict = {
api.node.DISCOVERING_STATE: _('Discovering'),
api.node.DISCOVERED_STATE: _('Discovered'),
api.node.PROVISIONED_STATE: _('Provisioned'),
api.node.PROVISIONING_FAILED_STATE: _('Provisioning Failed'),
api.node.PROVISIONING_STATE: _('Provisioning'),
api.node.FREE_STATE: _('Free'),
}
node_state = node.state
return state_dict.get(node_state, node_state)
class BaseNodesTable(tables.DataTable):
node = tables.Column('uuid',
link="horizon:infrastructure:nodes:detail",
verbose_name=_("Node Name"))
instance_ip = tables.Column(lambda n:
n.ip_address 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(get_power_state_with_transition,
verbose_name=_("Power"))
cpus = tables.Column('cpus',
verbose_name=_("CPU (cores)"))
memory_mb = tables.Column('memory_mb',
verbose_name=_("Memory (MB)"))
local_gb = tables.Column('local_gb',
verbose_name=_("Disk (GB)"))
power_status = tables.Column(get_power_state_with_transition,
verbose_name=_("Power Status"))
state = tables.Column(get_state_string,
verbose_name=_("Status"))
class Meta:
name = "nodes_table"
verbose_name = _("Registered Nodes")
verbose_name = _("Nodes")
table_actions = (NodeFilterAction, SetPowerStateOn, SetPowerStateOff,
DeleteNode)
row_actions = (SetPowerStateOn, SetPowerStateOff, DeleteNode)
@ -166,32 +183,51 @@ class RegisteredNodesTable(tables.DataTable):
return datum.uuid
class MaintenanceNodesTable(tables.DataTable):
node = tables.Column('uuid',
link="horizon:infrastructure:nodes:detail",
verbose_name=_("Node Name"))
cpu_arch = tables.Column('cpu_arch',
verbose_name=_("Arch."))
cpus = tables.Column('cpus',
verbose_name=_("CPU (cores)"))
memory_mb = tables.Column('memory_mb',
verbose_name=_("Memory (MB)"))
local_gb = tables.Column('local_gb',
verbose_name=_("Disk (GB)"))
driver = tables.Column('driver',
verbose_name=_("Driver"))
nics = tables.Column(lambda n: len(n.addresses),
verbose_name=_("NICs"))
class AllNodesTable(BaseNodesTable):
class Meta:
name = "all_nodes_table"
verbose_name = _("All")
columns = ('node', 'cpus', 'memory_mb', 'local_gb', 'power_status',
'state')
table_actions = (NodeFilterAction, SetPowerStateOn, SetPowerStateOff,
DeleteNode)
row_actions = (SetPowerStateOn, SetPowerStateOff, DeleteNode)
template = "horizon/common/_enhanced_data_table.html"
class ProvisionedNodesTable(BaseNodesTable):
class Meta:
name = "provisioned_nodes_table"
verbose_name = _("Provisioned")
table_actions = (NodeFilterAction, SetPowerStateOn, SetPowerStateOff,
DeleteNode)
row_actions = (SetPowerStateOn, SetPowerStateOff, DeleteNode)
template = "horizon/common/_enhanced_data_table.html"
class FreeNodesTable(BaseNodesTable):
class Meta:
name = "free_nodes_table"
verbose_name = _("Free")
columns = ('node', 'cpus', 'memory_mb', 'local_gb', 'power_status')
table_actions = (NodeFilterAction, SetPowerStateOn, SetPowerStateOff,
DeleteNode)
row_actions = (SetPowerStateOn, SetPowerStateOff, DeleteNode,)
template = "horizon/common/_enhanced_data_table.html"
class MaintenanceNodesTable(BaseNodesTable):
class Meta:
name = "maintenance_nodes_table"
verbose_name = _("Nodes (Maintenance)")
table_actions = (NodeFilterAction, ActivateNode, DeleteNode)
row_actions = (ActivateNode, DeleteNode,)
verbose_name = _("Maintenance")
columns = ('node', 'cpus', 'memory_mb', 'local_gb', 'power_status',
'state')
table_actions = (NodeFilterAction, ActivateNode, SetPowerStateOn,
SetPowerStateOff, DeleteNode)
row_actions = (ActivateNode, SetPowerStateOn, SetPowerStateOff,
DeleteNode)
template = "horizon/common/_enhanced_data_table.html"
def get_object_id(self, datum):
return datum.uuid
def get_object_display(self, datum):
return datum.uuid

View File

@ -81,38 +81,28 @@ class OverviewTab(tabs.Tab):
return context
class RegisteredTab(tabs.TableTab):
table_classes = (tables.RegisteredNodesTable,)
name = _("Registered")
slug = "registered"
class BaseTab(tabs.TableTab):
table_classes = (tables.BaseNodesTable,)
name = _("Nodes")
slug = "nodes"
template_name = "horizon/common/_detail_table.html"
def __init__(self, tab_group, request):
super(RegisteredTab, self).__init__(tab_group, request)
def get_items_count(self):
return len(self._nodes)
super(BaseTab, self).__init__(tab_group, request)
@cached_property
def _nodes(self):
redirect = urlresolvers.reverse('horizon:infrastructure:nodes:index')
return []
if 'provisioned' in self.request.GET:
associated = True
elif 'free' in self.request.GET:
associated = False
else:
associated = None
return api.node.Node.list(self.request, associated=associated,
maintenance=False, _error_redirect=redirect)
def get_items_count(self):
return len(self._nodes)
@cached_property
def _nodes_info(self):
page_size = functions.get_page_size(self.request)
prev_marker = self.request.GET.get(
tables.RegisteredNodesTable._meta.prev_pagination_param, None)
self.table_classes[0]._meta.prev_pagination_param, None)
if prev_marker is not None:
sort_dir = 'asc'
@ -120,7 +110,7 @@ class RegisteredTab(tabs.TableTab):
else:
sort_dir = 'desc'
marker = self.request.GET.get(
tables.RegisteredNodesTable._meta.pagination_param, None)
self.table_classes[0]._meta.pagination_param, None)
nodes = self._nodes
@ -141,7 +131,59 @@ class RegisteredTab(tabs.TableTab):
more = len(nodes) > end
return nodes[start:end], prev, more
def get_nodes_table_data(self):
def get_base_nodes_table_data(self):
nodes, prev, more = self._nodes_info
if 'errors' in self.request.GET:
return api.node.filter_nodes(nodes, healthy=False)
return nodes
def has_prev_data(self, table):
return self._nodes_info[1]
def has_more_data(self, table):
return self._nodes_info[2]
class AllTab(BaseTab):
table_classes = (tables.AllNodesTable,)
name = _("All")
slug = "all"
def __init__(self, tab_group, request):
super(AllTab, self).__init__(tab_group, request)
@cached_property
def _nodes(self):
redirect = urlresolvers.reverse('horizon:infrastructure:nodes:index')
return api.node.Node.list(self.request, _error_redirect=redirect)
def get_all_nodes_table_data(self):
nodes, prev, more = self._nodes_info
if 'errors' in self.request.GET:
return api.node.filter_nodes(nodes, healthy=False)
return nodes
class ProvisionedTab(BaseTab):
table_classes = (tables.ProvisionedNodesTable,)
name = _("Provisioned")
slug = "provisioned"
def __init__(self, tab_group, request):
super(ProvisionedTab, self).__init__(tab_group, request)
@cached_property
def _nodes(self):
redirect = urlresolvers.reverse('horizon:infrastructure:nodes:index')
return api.node.Node.list(self.request, associated=True,
maintenance=False, _error_redirect=redirect)
def get_provisioned_nodes_table_data(self):
nodes, prev, more = self._nodes_info
if 'errors' in self.request.GET:
@ -161,27 +203,46 @@ class RegisteredTab(tabs.TableTab):
return nodes
def has_prev_data(self, table):
return self._nodes_info[1]
def has_more_data(self, table):
return self._nodes_info[2]
class FreeTab(BaseTab):
table_classes = (tables.FreeNodesTable,)
name = _("Free")
slug = "free"
def __init__(self, tab_group, request):
super(FreeTab, self).__init__(tab_group, request)
@cached_property
def _nodes(self):
redirect = urlresolvers.reverse('horizon:infrastructure:nodes:index')
return api.node.Node.list(self.request, associated=False,
maintenance=False, _error_redirect=redirect)
def get_free_nodes_table_data(self):
nodes, prev, more = self._nodes_info
if 'errors' in self.request.GET:
return api.node.filter_nodes(nodes, healthy=False)
return nodes
class MaintenanceTab(tabs.TableTab):
class MaintenanceTab(BaseTab):
table_classes = (tables.MaintenanceNodesTable,)
name = _("Maintenance")
slug = "maintenance"
template_name = "horizon/common/_detail_table.html"
def get_items_count(self):
return len(self.get_maintenance_nodes_table_data())
def __init__(self, tab_group, request):
super(MaintenanceTab, self).__init__(tab_group, request)
@cached_property
def _nodes(self):
redirect = urlresolvers.reverse('horizon:infrastructure:nodes:index')
return api.node.Node.list(self.request, maintenance=True,
_error_redirect=redirect)
def get_maintenance_nodes_table_data(self):
redirect = urlresolvers.reverse('horizon:infrastructure:nodes:index')
nodes = api.node.Node.list(self.request, maintenance=True,
_error_redirect=redirect)
return nodes
return self._nodes
class DetailOverviewTab(tabs.Tab):
@ -226,7 +287,7 @@ class DetailOverviewTab(tabs.Tab):
class NodeTabs(tabs.TabGroup):
slug = "nodes"
tabs = (OverviewTab, RegisteredTab)
tabs = (OverviewTab, AllTab, ProvisionedTab, FreeTab, MaintenanceTab,)
sticky = True
template_name = "horizon/common/_items_count_tab_group.html"

View File

@ -7,7 +7,7 @@
<div class="widget">
<h3>{% trans 'Hardware Inventory' %}</h3>
<div class="widget-info">
<a href="{% url 'horizon:infrastructure:nodes:index' %}?tab=nodes__registered">
<a href="{% url 'horizon:infrastructure:nodes:index' %}?tab=nodes__all">
<span class="info">{{ nodes_all_count }} {% trans 'nodes' %}</span>
</a>
</div>
@ -29,7 +29,7 @@
<div class="col-xs-12">
<h3>{% trans "Provisioned nodes" %}</h3>
<div class="widget-info">
<a href="{% url 'horizon:infrastructure:nodes:index' %}?tab=nodes__registered&provisioned">
<a href="{% url 'horizon:infrastructure:nodes:index' %}?tab=nodes__provisioned">
<span class="info">{{ nodes_provisioned_count }} {% trans 'provisioned nodes' %}</span>
</a>
</div>

View File

@ -55,73 +55,71 @@ 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, 4)
self.assertEqual(mock.list.call_count, 7)
self.assertTemplateUsed(
res, 'infrastructure/nodes/index.html')
self.assertTemplateUsed(res, 'infrastructure/nodes/_overview.html')
def test_registered_nodes(self):
registered_nodes = [api.node.Node(api.node.IronicNode(node))
for node in self.ironicclient_nodes.list()]
roles = [api.tuskar.Role(r)
for r in TEST_DATA.tuskarclient_roles.list()]
instance = TEST_DATA.novaclient_servers.first()
image = TEST_DATA.glanceclient_images.first()
def _test_index_tab(self, tab_name):
nodes = [api.node.Node(api.node.IronicNode(node))
for node in self.ironicclient_nodes.list()]
# TODO(akrivoka): this should be placed in the test data, but currently
# that's not possible due to the drawbacks in the Node architecture.
# We should rework the entire api/node.py and fix this problem.
for node in registered_nodes:
for node in nodes:
node.ip_address = '1.1.1.1'
with contextlib.nested(
patch('tuskar_ui.api.tuskar.Role', **{
'spec_set': ['list', 'name'],
'list.return_value': roles,
}),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': registered_nodes,
}),
patch('tuskar_ui.api.node.nova', **{
'spec_set': ['server_get', 'server_list'],
'server_get.return_value': instance,
'server_list.return_value': ([instance], False),
}),
patch('tuskar_ui.api.node.glance', **{
'spec_set': ['image_get'],
'image_get.return_value': image,
}),
patch('tuskar_ui.api.heat.Resource', **{
'spec_set': ['get_by_node', 'list_all_resources'],
'get_by_node.side_effect': (
self._raise_horizon_exception_not_found),
'list_all_resources.return_value': [],
}),
) as (_Role, Node, _nova, _glance, _resource):
res = self.client.get(INDEX_URL + '?tab=nodes__registered')
with patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': nodes,
}) as Node:
res = self.client.get(INDEX_URL + '?tab=nodes__' + tab_name)
# FIXME(lsmola) horrible count, optimize
self.assertEqual(Node.list.call_count, 4)
self.assertEqual(Node.list.call_count, 7)
self.assertTemplateUsed(
res, 'infrastructure/nodes/index.html')
self.assertTemplateUsed(res, 'horizon/common/_detail_table.html')
self.assertItemsEqual(res.context['nodes_table_table'].data,
registered_nodes)
self.assertItemsEqual(
res.context[tab_name + '_nodes_table_table'].data,
nodes)
def test_registered_nodes_list_exception(self):
instance = TEST_DATA.novaclient_servers.first()
def test_all_nodes(self):
self._test_index_tab('all')
def test_provisioned_nodes(self):
self._test_index_tab('provisioned')
def test_free_nodes(self):
self._test_index_tab('free')
def test_maintenance_nodes(self):
self._test_index_tab('maintenance')
def _test_index_tab_list_exception(self, tab_name):
with patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list', 'instance'],
'instance': instance,
'spec_set': ['list'],
'list.side_effect': self._raise_tuskar_exception,
}) as mock:
res = self.client.get(INDEX_URL + '?tab=nodes__registered')
res = self.client.get(INDEX_URL + '?tab=nodes__' + tab_name)
self.assertEqual(mock.list.call_count, 4)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_all_nodes_list_exception(self):
self._test_index_tab_list_exception('all')
def test_provisioned_nodes_list_exception(self):
self._test_index_tab_list_exception('provisioned')
def test_free_nodes_list_exception(self):
self._test_index_tab_list_exception('free')
def test_maintenance_nodes_list_exception(self):
self._test_index_tab_list_exception('maintenance')
def test_register_get(self):
res = self.client.get(REGISTER_URL)
self.assertTemplateUsed(
@ -280,14 +278,14 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_node_set_power_on(self):
registered_nodes = [api.node.Node(api.node.IronicNode(node))
for node in self.ironicclient_nodes.list()]
node = registered_nodes[6]
all_nodes = [api.node.Node(api.node.IronicNode(node))
for node in self.ironicclient_nodes.list()]
node = all_nodes[6]
roles = [api.tuskar.Role(r)
for r in TEST_DATA.tuskarclient_roles.list()]
instance = TEST_DATA.novaclient_servers.first()
image = TEST_DATA.glanceclient_images.first()
data = {'action': "nodes_table__set_power_state_on__{0}".format(
data = {'action': "all_nodes_table__set_power_state_on__{0}".format(
node.uuid)}
with contextlib.nested(
@ -297,7 +295,7 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
}),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list', 'set_power_state'],
'list.return_value': registered_nodes,
'list.return_value': all_nodes,
'set_power_state.return_value': node,
}),
patch('tuskar_ui.api.tuskar.Role', **{
@ -321,21 +319,21 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
}),
) as (mock_node_client, mock_node, mock_role, mock_nova, mock_glance,
mock_resource):
res = self.client.post(INDEX_URL + '?tab=nodes__registered', data)
res = self.client.post(INDEX_URL + '?tab=nodes__all', data)
self.assertNoFormErrors(res)
self.assertEqual(mock_node.set_power_state.call_count, 1)
self.assertRedirectsNoFollow(res,
INDEX_URL + '?tab=nodes__registered')
INDEX_URL + '?tab=nodes__all')
def test_node_set_power_off(self):
registered_nodes = [api.node.Node(api.node.IronicNode(node))
for node in self.ironicclient_nodes.list()]
node = registered_nodes[8]
all_nodes = [api.node.Node(api.node.IronicNode(node))
for node in self.ironicclient_nodes.list()]
node = all_nodes[8]
roles = [api.tuskar.Role(r)
for r in TEST_DATA.tuskarclient_roles.list()]
instance = TEST_DATA.novaclient_servers.first()
image = TEST_DATA.glanceclient_images.first()
data = {'action': "nodes_table__set_power_state_off__{0}".format(
data = {'action': "all_nodes_table__set_power_state_off__{0}".format(
node.uuid)}
with contextlib.nested(
@ -345,7 +343,7 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
}),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list', 'set_power_state'],
'list.return_value': registered_nodes,
'list.return_value': all_nodes,
'set_power_state.return_value': node,
}),
patch('tuskar_ui.api.tuskar.Role', **{
@ -369,11 +367,11 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
}),
) as (mock_node_client, mock_node, mock_role, mock_nova, mock_glance,
mock_resource):
res = self.client.post(INDEX_URL + '?tab=nodes__registered', data)
res = self.client.post(INDEX_URL + '?tab=nodes__all', data)
self.assertNoFormErrors(res)
self.assertEqual(mock_node.set_power_state.call_count, 1)
self.assertRedirectsNoFollow(res,
INDEX_URL + '?tab=nodes__registered')
INDEX_URL + '?tab=nodes__all')
def test_performance(self):
node = api.node.Node(self.ironicclient_nodes.list()[0])

View File

@ -110,7 +110,7 @@ class DetailView(horizon_tabs.TabView):
if node.maintenance:
table = tables.MaintenanceNodesTable(self.request)
else:
table = tables.RegisteredNodesTable(self.request)
table = tables.ProvisionedNodesTable(self.request)
context['node'] = node
context['title'] = _("Node: %(uuid)s") % {'uuid': node.uuid}

View File

@ -55,7 +55,7 @@ class RolesTable(tables.DataTable):
template = "horizon/common/_enhanced_data_table.html"
class NodeTable(nodes_tables.RegisteredNodesTable):
class NodeTable(nodes_tables.ProvisionedNodesTable):
class Meta:
name = "nodetable"

View File

@ -134,8 +134,10 @@ def data(TEST):
},
'power_state': 'on',
'target_power_state': 'on',
'provision_state': 'active',
'maintenance': None,
'newly_discovered': None,
'extra': {}
})
node_2 = node.Node(
node.NodeManager(None),
@ -157,8 +159,10 @@ def data(TEST):
},
'power_state': 'on',
'target_power_state': 'on',
'provision_state': 'active',
'maintenance': None,
'newly_discovered': None,
'extra': {}
})
node_3 = node.Node(
node.NodeManager(None),
@ -180,8 +184,10 @@ def data(TEST):
},
'power_state': 'rebooting',
'target_power_state': 'on',
'provision_state': 'active',
'maintenance': None,
'newly_discovered': None,
'extra': {}
})
node_4 = node.Node(
node.NodeManager(None),
@ -203,8 +209,10 @@ def data(TEST):
},
'power_state': 'on',
'target_power_state': 'on',
'provision_state': 'active',
'maintenance': None,
'newly_discovered': None,
'extra': {}
})
node_5 = node.Node(
node.NodeManager(None),
@ -226,8 +234,10 @@ def data(TEST):
},
'power_state': 'error',
'target_power_state': 'on',
'provision_state': 'error',
'maintenance': None,
'newly_discovered': None,
'extra': {}
})
node_6 = node.Node(
node.NodeManager(None),
@ -249,8 +259,10 @@ def data(TEST):
},
'power_state': 'on',
'target_power_state': 'on',
'provision_state': 'active',
'maintenance': None,
'newly_discovered': None,
'extra': {}
})
node_7 = node.Node(
node.NodeManager(None),
@ -274,6 +286,7 @@ def data(TEST):
'target_power_state': 'on',
'maintenance': True,
'newly_discovered': None,
'extra': {}
})
node_8 = node.Node(
node.NodeManager(None),
@ -297,6 +310,7 @@ def data(TEST):
'target_power_state': 'on',
'maintenance': True,
'newly_discovered': True,
'extra': {}
})
node_9 = node.Node(
node.NodeManager(None),
@ -320,6 +334,7 @@ def data(TEST):
'target_power_state': 'on',
'maintenance': True,
'newly_discovered': True,
'extra': {}
})
TEST.ironicclient_nodes.add(node_1, node_2, node_3, node_4, node_5, node_6,
node_7, node_8, node_9)