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:
parent
59e6cc2531
commit
c4ac732aa9
|
@ -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__,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
|
@ -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")
|
|
@ -1,5 +0,0 @@
|
|||
<ul>
|
||||
{% for host in aggregate.hosts %}
|
||||
<li>{{ host }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
|
@ -1,5 +0,0 @@
|
|||
<ul>
|
||||
{% for key, value in aggregate.metadata.iteritems %}
|
||||
<li>{{ key }} = {{ value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
|
@ -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 %}
|
|
@ -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)
|
|
@ -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')
|
||||
)
|
|
@ -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
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>'])
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue