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)
('up', True),
('active', True),
('yes', True),
('on', True),
('none', None),
('unknown', None),
@ -118,6 +119,7 @@ class Column(html.HTMLElement):
('down', False),
('false', False),
('inactive', False),
('no', False),
('off', False),
)
@ -168,6 +170,12 @@ class Column(html.HTMLElement):
is displayed as a link.
Example: ``classes=('link-foo', 'link-bar')``.
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 = {
"sum": sum,
@ -183,6 +191,7 @@ class Column(html.HTMLElement):
('enabled', True),
('true', True),
('up', True),
('yes', True),
('active', True),
('on', True),
('none', None),
@ -192,6 +201,7 @@ class Column(html.HTMLElement):
('down', False),
('false', False),
('inactive', False),
('no', False),
('off', False),
)
@ -199,7 +209,7 @@ class Column(html.HTMLElement):
link=None, allowed_data_types=[], hidden=False, attrs=None,
status=False, status_choices=None, display_choices=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", []))
super(Column, self).__init__()
self.attrs.update(attrs or {})
@ -228,6 +238,7 @@ class Column(html.HTMLElement):
self.filters = filters or []
self.truncate = truncate
self.link_classes = link_classes or []
self.wrap_list = wrap_list
if status_choices:
self.status_choices = status_choices
@ -542,6 +553,7 @@ class Cell(html.HTMLElement):
self.data = data
self.column = column
self.row = row
self.wrap_list = column.wrap_list
def __repr__(self):
return '<%s: %s, %s>' % (self.__class__.__name__,

View File

@ -1,3 +1,3 @@
<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>

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):
slug = "admin"
name = _("System Panel")
panels = ('overview', 'aggregates', 'hypervisors', 'instances', 'volumes',
panels = ('overview', 'hypervisors', 'instances', 'volumes',
'flavors', 'images', 'networks', 'routers', 'info')

View File

@ -1,7 +1,7 @@
import logging
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 horizon import tables
@ -89,6 +89,41 @@ class ServicesTable(tables.DataTable):
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):
def filter(self, table, services, filter_string):
q = filter_string.lower()
@ -109,7 +144,7 @@ class NovaServicesTable(tables.DataTable):
state = tables.Column('state', verbose_name=_('State'))
updated_at = tables.Column('updated_at',
verbose_name=_('Updated At'),
filters=(parse_isotime, timesince))
filters=(parse_isotime, filters.timesince))
def get_object_id(self, obj):
return "%s-%s-%s" % (obj.binary, obj.host, obj.zone)
@ -119,3 +154,31 @@ class NovaServicesTable(tables.DataTable):
verbose_name = _("Compute Services")
table_actions = (NovaServiceFilterAction,)
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.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 QuotasTable
from openstack_dashboard.dashboards.admin.info.tables import ServicesTable
from openstack_dashboard.dashboards.admin.info.tables import ZonesTable
class DefaultQuotasTab(tabs.TableTab):
@ -68,6 +70,39 @@ class ServicesTab(tabs.TableTab):
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):
table_classes = (NovaServicesTable,)
name = _("Compute Services")
@ -88,5 +123,6 @@ class NovaServicesTab(tabs.TableTab):
class SystemInfoTabs(tabs.TabGroup):
slug = "system_info"
tabs = (ServicesTab, NovaServicesTab, DefaultQuotasTab,)
tabs = (ServicesTab, NovaServicesTab, ZonesTab, HostAggregatesTab,
DefaultQuotasTab)
sticky = True

View File

@ -24,17 +24,24 @@ from openstack_dashboard.test import helpers as test
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',)})
def test_index(self):
api.nova.default_quota_get(IsA(http.HttpRequest),
self.tenant.id).AndReturn(self.quotas.nova)
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()
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()
@ -68,8 +75,19 @@ class ServicesViewTests(test.BaseAdminViewTests):
'<Quota: (security_group_rules, 20)>'],
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',),
api.nova: ('default_quota_get', 'service_list',),
api.nova: ('default_quota_get',
'service_list',
'availability_zone_list',
'aggregate_list'),
api.cinder: ('default_quota_get',)})
def test_index_with_neutron_disabled(self):
# 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),
self.tenant.id).AndReturn(self.quotas.nova)
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()
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()
@ -118,3 +141,11 @@ class ServicesViewTests(test.BaseAdminViewTests):
'<Quota: (security_groups, 10)>',
'<Quota: (security_group_rules, 20)>'],
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(
availability_zones.AvailabilityZone(
availability_zones.AvailabilityZoneManager(None),
{'zoneName': 'nova', 'zoneState': {'available': True}}
{
'zoneName': 'nova',
'zoneState': {'available': True},
'hosts': {
"host001": {
"nova-network": {
"active": True,
"available": True
}
}
}
}
)
)