From c4ac732aa9403518affb2f0929a73d42a14df353 Mon Sep 17 00:00:00 2001 From: Gabriel Hurley Date: Fri, 9 Aug 2013 15:47:58 -0700 Subject: [PATCH] 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 --- horizon/tables/base.py | 14 +++- .../horizon/common/_data_table_row.html | 2 +- .../dashboards/admin/aggregates/__init__.py | 0 .../dashboards/admin/aggregates/panel.py | 29 -------- .../dashboards/admin/aggregates/tables.py | 54 --------------- .../aggregates/_aggregate_hosts.html | 5 -- .../aggregates/_aggregate_metadata.html | 5 -- .../templates/aggregates/index.html | 11 --- .../dashboards/admin/aggregates/tests.py | 34 ---------- .../dashboards/admin/aggregates/urls.py | 27 -------- .../dashboards/admin/aggregates/views.py | 42 ------------ .../dashboards/admin/dashboard.py | 2 +- .../dashboards/admin/info/tables.py | 67 ++++++++++++++++++- .../dashboards/admin/info/tabs.py | 38 ++++++++++- .../dashboards/admin/info/tests.py | 41 ++++++++++-- .../test/test_data/nova_data.py | 13 +++- 16 files changed, 165 insertions(+), 219 deletions(-) delete mode 100644 openstack_dashboard/dashboards/admin/aggregates/__init__.py delete mode 100644 openstack_dashboard/dashboards/admin/aggregates/panel.py delete mode 100644 openstack_dashboard/dashboards/admin/aggregates/tables.py delete mode 100644 openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_hosts.html delete mode 100644 openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_metadata.html delete mode 100644 openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html delete mode 100644 openstack_dashboard/dashboards/admin/aggregates/tests.py delete mode 100644 openstack_dashboard/dashboards/admin/aggregates/urls.py delete mode 100644 openstack_dashboard/dashboards/admin/aggregates/views.py diff --git a/horizon/tables/base.py b/horizon/tables/base.py index 3a00cb6d5a..1fdaf60744 100644 --- a/horizon/tables/base.py +++ b/horizon/tables/base.py @@ -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 ```` 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__, diff --git a/horizon/templates/horizon/common/_data_table_row.html b/horizon/templates/horizon/common/_data_table_row.html index 3e0c8b0f82..d6df2ffab5 100644 --- a/horizon/templates/horizon/common/_data_table_row.html +++ b/horizon/templates/horizon/common/_data_table_row.html @@ -1,3 +1,3 @@ - {% for cell in row %}{{ cell.value }}{% endfor %} + {% for cell in row %}{%if cell.wrap_list %}
    {% endif %}{{ cell.value }}{%if cell.wrap_list %}
{% endif %}{% endfor %} diff --git a/openstack_dashboard/dashboards/admin/aggregates/__init__.py b/openstack_dashboard/dashboards/admin/aggregates/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openstack_dashboard/dashboards/admin/aggregates/panel.py b/openstack_dashboard/dashboards/admin/aggregates/panel.py deleted file mode 100644 index a95f83ed78..0000000000 --- a/openstack_dashboard/dashboards/admin/aggregates/panel.py +++ /dev/null @@ -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) diff --git a/openstack_dashboard/dashboards/admin/aggregates/tables.py b/openstack_dashboard/dashboards/admin/aggregates/tables.py deleted file mode 100644 index 378e1a18cc..0000000000 --- a/openstack_dashboard/dashboards/admin/aggregates/tables.py +++ /dev/null @@ -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") diff --git a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_hosts.html b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_hosts.html deleted file mode 100644 index c85c3e90ab..0000000000 --- a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_hosts.html +++ /dev/null @@ -1,5 +0,0 @@ -
    -{% for host in aggregate.hosts %} -
  • {{ host }}
  • -{% endfor %} -
diff --git a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_metadata.html b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_metadata.html deleted file mode 100644 index ab8be7c330..0000000000 --- a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_metadata.html +++ /dev/null @@ -1,5 +0,0 @@ -
    -{% for key, value in aggregate.metadata.iteritems %} -
  • {{ key }} = {{ value }}
  • -{% endfor %} -
diff --git a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html deleted file mode 100644 index 42b7cab7da..0000000000 --- a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html +++ /dev/null @@ -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 %} diff --git a/openstack_dashboard/dashboards/admin/aggregates/tests.py b/openstack_dashboard/dashboards/admin/aggregates/tests.py deleted file mode 100644 index babcd7356d..0000000000 --- a/openstack_dashboard/dashboards/admin/aggregates/tests.py +++ /dev/null @@ -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) diff --git a/openstack_dashboard/dashboards/admin/aggregates/urls.py b/openstack_dashboard/dashboards/admin/aggregates/urls.py deleted file mode 100644 index f8649a2f9e..0000000000 --- a/openstack_dashboard/dashboards/admin/aggregates/urls.py +++ /dev/null @@ -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') -) diff --git a/openstack_dashboard/dashboards/admin/aggregates/views.py b/openstack_dashboard/dashboards/admin/aggregates/views.py deleted file mode 100644 index 2d1a48a870..0000000000 --- a/openstack_dashboard/dashboards/admin/aggregates/views.py +++ /dev/null @@ -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 diff --git a/openstack_dashboard/dashboards/admin/dashboard.py b/openstack_dashboard/dashboards/admin/dashboard.py index 9291aea5a3..ce4456c7ec 100644 --- a/openstack_dashboard/dashboards/admin/dashboard.py +++ b/openstack_dashboard/dashboards/admin/dashboard.py @@ -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') diff --git a/openstack_dashboard/dashboards/admin/info/tables.py b/openstack_dashboard/dashboards/admin/info/tables.py index 56a3cabb5f..7ab85a2cc4 100644 --- a/openstack_dashboard/dashboards/admin/info/tables.py +++ b/openstack_dashboard/dashboards/admin/info/tables.py @@ -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") diff --git a/openstack_dashboard/dashboards/admin/info/tabs.py b/openstack_dashboard/dashboards/admin/info/tabs.py index b2ce71515e..39b93d76b4 100644 --- a/openstack_dashboard/dashboards/admin/info/tabs.py +++ b/openstack_dashboard/dashboards/admin/info/tabs.py @@ -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 diff --git a/openstack_dashboard/dashboards/admin/info/tests.py b/openstack_dashboard/dashboards/admin/info/tests.py index a42ed5465a..39deea1712 100644 --- a/openstack_dashboard/dashboards/admin/info/tests.py +++ b/openstack_dashboard/dashboards/admin/info/tests.py @@ -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): ''], ordered=False) + zones_tab = res.context['tab_group'].get_tab('zones') + self.assertQuerysetEqual(zones_tab._tables['zones'].data, + ['']) + + aggregates_tab = res.context['tab_group'].get_tab('aggregates') + self.assertQuerysetEqual(aggregates_tab._tables['aggregates'].data, + ['', '']) + @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): '', ''], ordered=False) + + zones_tab = res.context['tab_group'].get_tab('zones') + self.assertQuerysetEqual(zones_tab._tables['zones'].data, + ['']) + + aggregates_tab = res.context['tab_group'].get_tab('aggregates') + self.assertQuerysetEqual(aggregates_tab._tables['aggregates'].data, + ['', '']) diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index 34f7a08715..1c5f0b10a3 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -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 + } + } + } + } ) )