Adds availability zone info to System Info panel

Also moves the host aggregates data into System Info panel
because it's exactly the type of static information that
belongs there, and it reduces a significant amount of
boilerplate.

Incidentally adds a new table feature to make listing
out unordered lists of items easier.

Implements blueprint show-zone-for-admin

Change-Id: Id8fb5c9615b018135a5d90eaa82eb80ff63bb7dc
This commit is contained in:
Gabriel Hurley 2013-08-09 15:47:58 -07:00
parent 59e6cc2531
commit c4ac732aa9
16 changed files with 165 additions and 219 deletions

View File

@ -110,6 +110,7 @@ class Column(html.HTMLElement):
('true', True) ('true', True)
('up', True), ('up', True),
('active', True), ('active', True),
('yes', True),
('on', True), ('on', True),
('none', None), ('none', None),
('unknown', None), ('unknown', None),
@ -118,6 +119,7 @@ class Column(html.HTMLElement):
('down', False), ('down', False),
('false', False), ('false', False),
('inactive', False), ('inactive', False),
('no', False),
('off', False), ('off', False),
) )
@ -168,6 +170,12 @@ class Column(html.HTMLElement):
is displayed as a link. is displayed as a link.
Example: ``classes=('link-foo', 'link-bar')``. Example: ``classes=('link-foo', 'link-bar')``.
Defaults to ``None``. Defaults to ``None``.
.. attribute:: wrap_list
Boolean value indicating whether the contents of this cell should be
wrapped in a ``<ul></ul>`` tag. Useful in conjunction with Django's
``unordered_list`` template filter. Defaults to ``False``.
""" """
summation_methods = { summation_methods = {
"sum": sum, "sum": sum,
@ -183,6 +191,7 @@ class Column(html.HTMLElement):
('enabled', True), ('enabled', True),
('true', True), ('true', True),
('up', True), ('up', True),
('yes', True),
('active', True), ('active', True),
('on', True), ('on', True),
('none', None), ('none', None),
@ -192,6 +201,7 @@ class Column(html.HTMLElement):
('down', False), ('down', False),
('false', False), ('false', False),
('inactive', False), ('inactive', False),
('no', False),
('off', False), ('off', False),
) )
@ -199,7 +209,7 @@ class Column(html.HTMLElement):
link=None, allowed_data_types=[], hidden=False, attrs=None, link=None, allowed_data_types=[], hidden=False, attrs=None,
status=False, status_choices=None, display_choices=None, status=False, status_choices=None, display_choices=None,
empty_value=None, filters=None, classes=None, summation=None, empty_value=None, filters=None, classes=None, summation=None,
auto=None, truncate=None, link_classes=None): auto=None, truncate=None, link_classes=None, wrap_list=False):
self.classes = list(classes or getattr(self, "classes", [])) self.classes = list(classes or getattr(self, "classes", []))
super(Column, self).__init__() super(Column, self).__init__()
self.attrs.update(attrs or {}) self.attrs.update(attrs or {})
@ -228,6 +238,7 @@ class Column(html.HTMLElement):
self.filters = filters or [] self.filters = filters or []
self.truncate = truncate self.truncate = truncate
self.link_classes = link_classes or [] self.link_classes = link_classes or []
self.wrap_list = wrap_list
if status_choices: if status_choices:
self.status_choices = status_choices self.status_choices = status_choices
@ -542,6 +553,7 @@ class Cell(html.HTMLElement):
self.data = data self.data = data
self.column = column self.column = column
self.row = row self.row = row
self.wrap_list = column.wrap_list
def __repr__(self): def __repr__(self):
return '<%s: %s, %s>' % (self.__class__.__name__, return '<%s: %s, %s>' % (self.__class__.__name__,

View File

@ -1,3 +1,3 @@
<tr{{ row.attr_string|safe }}> <tr{{ row.attr_string|safe }}>
{% for cell in row %}<td{{ cell.attr_string|safe }}>{{ cell.value }}</td>{% endfor %} {% for cell in row %}<td{{ cell.attr_string|safe }}>{%if cell.wrap_list %}<ul>{% endif %}{{ cell.value }}{%if cell.wrap_list %}</ul>{% endif %}</td>{% endfor %}
</tr> </tr>

View File

@ -1,29 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 B1 Systems GmbH
#
# 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
from openstack_dashboard.dashboards.admin import dashboard
class Aggregates(horizon.Panel):
name = _("Aggregates")
slug = 'aggregates'
permissions = ('openstack.roles.admin',)
dashboard.Admin.register(Aggregates)

View File

@ -1,54 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 B1 Systems GmbH
#
# 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.
import logging
from django import template
from django.utils.translation import ugettext_lazy as _
from horizon import tables
LOG = logging.getLogger(__name__)
def get_hosts(aggregate):
template_name = 'admin/aggregates/_aggregate_hosts.html'
context = {"aggregate": aggregate}
return template.loader.render_to_string(template_name, context)
def get_metadata(aggregate):
template_name = 'admin/aggregates/_aggregate_metadata.html'
context = {"aggregate": aggregate}
return template.loader.render_to_string(template_name, context)
class AdminAggregatesTable(tables.DataTable):
name = tables.Column("name",
verbose_name=_("Name"))
availability_zone = tables.Column("availability_zone",
verbose_name=_("Availability Zone"))
hosts = tables.Column(get_hosts,
verbose_name=_("Hosts"))
metadata = tables.Column(get_metadata,
verbose_name=_("Metadata"))
class Meta:
name = "aggregates"
verbose_name = _("Aggregates")

View File

@ -1,5 +0,0 @@
<ul>
{% for host in aggregate.hosts %}
<li>{{ host }}</li>
{% endfor %}
</ul>

View File

@ -1,5 +0,0 @@
<ul>
{% for key, value in aggregate.metadata.iteritems %}
<li>{{ key }} = {{ value }}</li>
{% endfor %}
</ul>

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Aggregates" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("All Aggregates") %}
{% endblock page_header %}
{% block main %}
{{ table.render }}
{% endblock %}

View File

@ -1,34 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 B1 Systems GmbH
#
# 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.core.urlresolvers import reverse
from django import http
from mox import IsA
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
class AggregateViewTest(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('aggregate_list',)})
def test_index(self):
aggregates = self.aggregates.list()
api.nova.aggregate_list(IsA(http.HttpRequest)).AndReturn(aggregates)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:admin:aggregates:index'))
self.assertTemplateUsed(res, 'admin/aggregates/index.html')
self.assertItemsEqual(res.context['table'].data, aggregates)

View File

@ -1,27 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 B1 Systems GmbH
#
# 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.conf.urls.defaults import patterns
from django.conf.urls.defaults import url
from openstack_dashboard.dashboards.admin.aggregates.views \
import AdminIndexView
urlpatterns = patterns(
'openstack_dashboard.dashboards.admin.aggregates.views',
url(r'^$', AdminIndexView.as_view(), name='index')
)

View File

@ -1,42 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 B1 Systems GmbH
#
# 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.
import logging
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.aggregates.tables import \
AdminAggregatesTable
LOG = logging.getLogger(__name__)
class AdminIndexView(tables.DataTableView):
table_class = AdminAggregatesTable
template_name = 'admin/aggregates/index.html'
def get_data(self):
aggregates = []
try:
aggregates = api.nova.aggregate_list(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve aggregate list.'))
return aggregates

View File

@ -22,7 +22,7 @@ import horizon
class SystemPanels(horizon.PanelGroup): class SystemPanels(horizon.PanelGroup):
slug = "admin" slug = "admin"
name = _("System Panel") name = _("System Panel")
panels = ('overview', 'aggregates', 'hypervisors', 'instances', 'volumes', panels = ('overview', 'hypervisors', 'instances', 'volumes',
'flavors', 'images', 'networks', 'routers', 'info') 'flavors', 'images', 'networks', 'routers', 'info')

View File

@ -1,7 +1,7 @@
import logging import logging
from django import template from django import template
from django.template.defaultfilters import timesince from django.template import defaultfilters as filters
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import tables from horizon import tables
@ -89,6 +89,41 @@ class ServicesTable(tables.DataTable):
status_columns = ["enabled"] status_columns = ["enabled"]
def get_available(zone):
return zone.zoneState['available']
def get_hosts(zone):
hosts = zone.hosts
host_details = []
for name, services in hosts.items():
up = all([s['active'] and s['available'] for k, s in services.items()])
up = _("Services Up") if up else _("Services Down")
host_details.append("%(host)s (%(up)s)" % {'host': name, 'up': up})
return host_details
class ZonesTable(tables.DataTable):
name = tables.Column('zoneName', verbose_name=_('Name'))
hosts = tables.Column(get_hosts,
verbose_name=_('Hosts'),
wrap_list=True,
filters=(filters.unordered_list,))
available = tables.Column(get_available,
verbose_name=_('Available'),
status=True,
filters=(filters.yesno, filters.capfirst))
def get_object_id(self, zone):
return zone.zoneName
class Meta:
name = "zones"
verbose_name = _("Availability Zones")
multi_select = False
status_columns = ["available"]
class NovaServiceFilterAction(tables.FilterAction): class NovaServiceFilterAction(tables.FilterAction):
def filter(self, table, services, filter_string): def filter(self, table, services, filter_string):
q = filter_string.lower() q = filter_string.lower()
@ -109,7 +144,7 @@ class NovaServicesTable(tables.DataTable):
state = tables.Column('state', verbose_name=_('State')) state = tables.Column('state', verbose_name=_('State'))
updated_at = tables.Column('updated_at', updated_at = tables.Column('updated_at',
verbose_name=_('Updated At'), verbose_name=_('Updated At'),
filters=(parse_isotime, timesince)) filters=(parse_isotime, filters.timesince))
def get_object_id(self, obj): def get_object_id(self, obj):
return "%s-%s-%s" % (obj.binary, obj.host, obj.zone) return "%s-%s-%s" % (obj.binary, obj.host, obj.zone)
@ -119,3 +154,31 @@ class NovaServicesTable(tables.DataTable):
verbose_name = _("Compute Services") verbose_name = _("Compute Services")
table_actions = (NovaServiceFilterAction,) table_actions = (NovaServiceFilterAction,)
multi_select = False multi_select = False
def get_hosts(aggregate):
return [host for host in aggregate.hosts]
def get_metadata(aggregate):
return [' = '.join([key, val]) for key, val
in aggregate.metadata.iteritems()]
class AggregatesTable(tables.DataTable):
name = tables.Column("name",
verbose_name=_("Name"))
availability_zone = tables.Column("availability_zone",
verbose_name=_("Availability Zone"))
hosts = tables.Column(get_hosts,
verbose_name=_("Hosts"),
wrap_list=True,
filters=(filters.unordered_list,))
metadata = tables.Column(get_metadata,
verbose_name=_("Metadata"),
wrap_list=True,
filters=(filters.unordered_list,))
class Meta:
name = "aggregates"
verbose_name = _("Host Aggregates")

View File

@ -24,9 +24,11 @@ from openstack_dashboard.api import keystone
from openstack_dashboard.api import nova from openstack_dashboard.api import nova
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.admin.info.tables import AggregatesTable
from openstack_dashboard.dashboards.admin.info.tables import NovaServicesTable from openstack_dashboard.dashboards.admin.info.tables import NovaServicesTable
from openstack_dashboard.dashboards.admin.info.tables import QuotasTable from openstack_dashboard.dashboards.admin.info.tables import QuotasTable
from openstack_dashboard.dashboards.admin.info.tables import ServicesTable from openstack_dashboard.dashboards.admin.info.tables import ServicesTable
from openstack_dashboard.dashboards.admin.info.tables import ZonesTable
class DefaultQuotasTab(tabs.TableTab): class DefaultQuotasTab(tabs.TableTab):
@ -68,6 +70,39 @@ class ServicesTab(tabs.TableTab):
return services return services
class ZonesTab(tabs.TableTab):
table_classes = (ZonesTable,)
name = _("Availability Zones")
slug = "zones"
template_name = ("horizon/common/_detail_table.html")
def get_zones_data(self):
request = self.tab_group.request
zones = []
try:
zones = nova.availability_zone_list(request, detailed=True)
except Exception:
msg = _('Unable to retrieve availability zone data.')
exceptions.handle(request, msg)
return zones
class HostAggregatesTab(tabs.TableTab):
table_classes = (AggregatesTable,)
name = _("Host Aggregates")
slug = "aggregates"
template_name = ("horizon/common/_detail_table.html")
def get_aggregates_data(self):
aggregates = []
try:
aggregates = nova.aggregate_list(self.tab_group.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve host aggregates list.'))
return aggregates
class NovaServicesTab(tabs.TableTab): class NovaServicesTab(tabs.TableTab):
table_classes = (NovaServicesTable,) table_classes = (NovaServicesTable,)
name = _("Compute Services") name = _("Compute Services")
@ -88,5 +123,6 @@ class NovaServicesTab(tabs.TableTab):
class SystemInfoTabs(tabs.TabGroup): class SystemInfoTabs(tabs.TabGroup):
slug = "system_info" slug = "system_info"
tabs = (ServicesTab, NovaServicesTab, DefaultQuotasTab,) tabs = (ServicesTab, NovaServicesTab, ZonesTab, HostAggregatesTab,
DefaultQuotasTab)
sticky = True sticky = True

View File

@ -24,17 +24,24 @@ from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:admin:info:index') INDEX_URL = reverse('horizon:admin:info:index')
class ServicesViewTests(test.BaseAdminViewTests): class SystemInfoViewTests(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('default_quota_get', 'service_list',), @test.create_stubs({api.nova: ('default_quota_get',
'service_list',
'availability_zone_list',
'aggregate_list'),
api.cinder: ('default_quota_get',)}) api.cinder: ('default_quota_get',)})
def test_index(self): def test_index(self):
api.nova.default_quota_get(IsA(http.HttpRequest), api.nova.default_quota_get(IsA(http.HttpRequest),
self.tenant.id).AndReturn(self.quotas.nova) self.tenant.id).AndReturn(self.quotas.nova)
api.cinder.default_quota_get(IsA(http.HttpRequest), self.tenant.id) \ api.cinder.default_quota_get(IsA(http.HttpRequest), self.tenant.id) \
.AndReturn(self.cinder_quotas.first()) .AndReturn(self.cinder_quotas.first())
services = self.services.list() services = self.services.list()
api.nova.service_list(IsA(http.HttpRequest)).AndReturn(services) api.nova.service_list(IsA(http.HttpRequest)).AndReturn(services)
api.nova.availability_zone_list(IsA(http.HttpRequest), detailed=True) \
.AndReturn(self.availability_zones.list())
api.nova.aggregate_list(IsA(http.HttpRequest)) \
.AndReturn(self.aggregates.list())
self.mox.ReplayAll() self.mox.ReplayAll()
@ -68,8 +75,19 @@ class ServicesViewTests(test.BaseAdminViewTests):
'<Quota: (security_group_rules, 20)>'], '<Quota: (security_group_rules, 20)>'],
ordered=False) ordered=False)
zones_tab = res.context['tab_group'].get_tab('zones')
self.assertQuerysetEqual(zones_tab._tables['zones'].data,
['<AvailabilityZone: nova>'])
aggregates_tab = res.context['tab_group'].get_tab('aggregates')
self.assertQuerysetEqual(aggregates_tab._tables['aggregates'].data,
['<Aggregate: 1>', '<Aggregate: 2>'])
@test.create_stubs({api.base: ('is_service_enabled',), @test.create_stubs({api.base: ('is_service_enabled',),
api.nova: ('default_quota_get', 'service_list',), api.nova: ('default_quota_get',
'service_list',
'availability_zone_list',
'aggregate_list'),
api.cinder: ('default_quota_get',)}) api.cinder: ('default_quota_get',)})
def test_index_with_neutron_disabled(self): def test_index_with_neutron_disabled(self):
# Neutron does not have an API for getting default system # Neutron does not have an API for getting default system
@ -80,10 +98,15 @@ class ServicesViewTests(test.BaseAdminViewTests):
api.nova.default_quota_get(IsA(http.HttpRequest), api.nova.default_quota_get(IsA(http.HttpRequest),
self.tenant.id).AndReturn(self.quotas.nova) self.tenant.id).AndReturn(self.quotas.nova)
api.cinder.default_quota_get(IsA(http.HttpRequest), self.tenant.id) \ api.cinder.default_quota_get(IsA(http.HttpRequest), self.tenant.id) \
.AndReturn(self.cinder_quotas.first()) .AndReturn(self.cinder_quotas.first())
services = self.services.list() services = self.services.list()
api.nova.service_list(IsA(http.HttpRequest)).AndReturn(services) api.nova.service_list(IsA(http.HttpRequest)).AndReturn(services)
api.nova.availability_zone_list(IsA(http.HttpRequest), detailed=True) \
.AndReturn(self.availability_zones.list())
api.nova.aggregate_list(IsA(http.HttpRequest)) \
.AndReturn(self.aggregates.list())
self.mox.ReplayAll() self.mox.ReplayAll()
@ -118,3 +141,11 @@ class ServicesViewTests(test.BaseAdminViewTests):
'<Quota: (security_groups, 10)>', '<Quota: (security_groups, 10)>',
'<Quota: (security_group_rules, 20)>'], '<Quota: (security_group_rules, 20)>'],
ordered=False) ordered=False)
zones_tab = res.context['tab_group'].get_tab('zones')
self.assertQuerysetEqual(zones_tab._tables['zones'].data,
['<AvailabilityZone: nova>'])
aggregates_tab = res.context['tab_group'].get_tab('aggregates')
self.assertQuerysetEqual(aggregates_tab._tables['aggregates'].data,
['<Aggregate: 1>', '<Aggregate: 2>'])

View File

@ -490,7 +490,18 @@ def data(TEST):
TEST.availability_zones.add( TEST.availability_zones.add(
availability_zones.AvailabilityZone( availability_zones.AvailabilityZone(
availability_zones.AvailabilityZoneManager(None), availability_zones.AvailabilityZoneManager(None),
{'zoneName': 'nova', 'zoneState': {'available': True}} {
'zoneName': 'nova',
'zoneState': {'available': True},
'hosts': {
"host001": {
"nova-network": {
"active": True,
"available": True
}
}
}
}
) )
) )