Merge "Host aggregates panel."
This commit is contained in:
commit
3a153c86af
@ -702,15 +702,42 @@ def service_list(request):
|
||||
return novaclient(request).services.list()
|
||||
|
||||
|
||||
def aggregate_list(request):
|
||||
def aggregate_details_list(request):
|
||||
result = []
|
||||
c = novaclient(request)
|
||||
for aggregate in c.aggregates.list():
|
||||
result.append(c.aggregates.get_details(aggregate.id))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def aggregate_create(request, name, availability_zone=None):
|
||||
return novaclient(request).aggregates.create(name, availability_zone)
|
||||
|
||||
|
||||
def aggregate_delete(request, aggregate_id):
|
||||
return novaclient(request).aggregates.delete(aggregate_id)
|
||||
|
||||
|
||||
def aggregate_get(request, aggregate_id):
|
||||
return novaclient(request).aggregates.get(aggregate_id)
|
||||
|
||||
|
||||
def aggregate_update(request, aggregate_id, values):
|
||||
return novaclient(request).aggregates.update(aggregate_id, values)
|
||||
|
||||
|
||||
def host_list(request):
|
||||
return novaclient(request).hosts.list()
|
||||
|
||||
|
||||
def add_host_to_aggregate(request, aggregate_id, host):
|
||||
return novaclient(request).aggregates.add_host(aggregate_id, host)
|
||||
|
||||
|
||||
def remove_host_from_aggregate(request, aggregate_id, host):
|
||||
return novaclient(request).aggregates.remove_host(aggregate_id, host)
|
||||
|
||||
|
||||
@memoized
|
||||
def list_extensions(request):
|
||||
return nova_list_extensions.ListExtManager(novaclient(request)).show_all()
|
||||
|
21
openstack_dashboard/dashboards/admin/aggregates/constants.py
Normal file
21
openstack_dashboard/dashboards/admin/aggregates/constants.py
Normal file
@ -0,0 +1,21 @@
|
||||
# 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.
|
||||
|
||||
AGGREGATES_TEMPLATE_NAME = 'admin/aggregates/index.html'
|
||||
AGGREGATES_INDEX_URL = 'horizon:admin:aggregates:index'
|
||||
AGGREGATES_INDEX_VIEW_TEMPLATE = 'admin/aggregates/index.html'
|
||||
AGGREGATES_CREATE_URL = 'horizon:admin:aggregates:create'
|
||||
AGGREGATES_CREATE_VIEW_TEMPLATE = 'admin/aggregates/create.html'
|
||||
AGGREGATES_MANAGE_HOSTS_URL = 'horizon:admin:aggregates:manage_hosts'
|
||||
AGGREGATES_MANAGE_HOSTS_TEMPLATE = 'admin/aggregates/manage_hosts.html'
|
||||
AGGREGATES_UPDATE_URL = 'horizon:admin:aggregates:update'
|
||||
AGGREGATES_UPDATE_VIEW_TEMPLATE = 'admin/aggregates/update.html'
|
48
openstack_dashboard/dashboards/admin/aggregates/forms.py
Normal file
48
openstack_dashboard/dashboards/admin/aggregates/forms.py
Normal file
@ -0,0 +1,48 @@
|
||||
# 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 _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.admin.aggregates import constants
|
||||
|
||||
INDEX_URL = constants.AGGREGATES_INDEX_URL
|
||||
|
||||
|
||||
class UpdateAggregateForm(forms.SelfHandlingForm):
|
||||
name = forms.CharField(max_length="255", label=_("Name"))
|
||||
availability_zone = forms.CharField(label=_("Availability zones"),
|
||||
required=False)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(UpdateAggregateForm, self).__init__(request, *args, **kwargs)
|
||||
|
||||
def handle(self, request, data):
|
||||
id = self.initial['id']
|
||||
name = data['name']
|
||||
availability_zone = data['availability_zone']
|
||||
aggregate = {'name': name}
|
||||
if availability_zone:
|
||||
aggregate['availability_zone'] = availability_zone
|
||||
try:
|
||||
api.nova.aggregate_update(request, id, aggregate)
|
||||
message = _('Successfully updated aggregate: "%s."') \
|
||||
% data['name']
|
||||
messages.success(request, message)
|
||||
except Exception:
|
||||
exceptions.handle(request,
|
||||
_('Unable to update the aggregate.'))
|
||||
return True
|
25
openstack_dashboard/dashboards/admin/aggregates/panel.py
Normal file
25
openstack_dashboard/dashboards/admin/aggregates/panel.py
Normal file
@ -0,0 +1,25 @@
|
||||
# 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 = _("Host Aggregates")
|
||||
slug = 'aggregates'
|
||||
|
||||
|
||||
dashboard.Admin.register(Aggregates)
|
127
openstack_dashboard/dashboards/admin/aggregates/tables.py
Normal file
127
openstack_dashboard/dashboards/admin/aggregates/tables.py
Normal file
@ -0,0 +1,127 @@
|
||||
# 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.template import defaultfilters as filters
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.admin.aggregates import constants
|
||||
|
||||
|
||||
class DeleteAggregateAction(tables.DeleteAction):
|
||||
data_type_singular = _("Host Aggregate")
|
||||
data_type_plural = _("Host Aggregates")
|
||||
|
||||
def delete(self, request, obj_id):
|
||||
api.nova.aggregate_delete(request, obj_id)
|
||||
|
||||
|
||||
class CreateAggregateAction(tables.LinkAction):
|
||||
name = "create"
|
||||
verbose_name = _("Create Host Aggregate")
|
||||
url = constants.AGGREGATES_CREATE_URL
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
|
||||
|
||||
class ManageHostsAction(tables.LinkAction):
|
||||
name = "manage"
|
||||
verbose_name = _("Manage Hosts")
|
||||
url = constants.AGGREGATES_MANAGE_HOSTS_URL
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
|
||||
|
||||
class UpdateAggregateAction(tables.LinkAction):
|
||||
name = "update"
|
||||
verbose_name = _("Edit Host Aggregate")
|
||||
url = constants.AGGREGATES_UPDATE_URL
|
||||
classes = ("ajax-modal", "btn-edit")
|
||||
|
||||
|
||||
class AggregateFilterAction(tables.FilterAction):
|
||||
def filter(self, table, aggregates, filter_string):
|
||||
q = filter_string.lower()
|
||||
|
||||
def comp(aggregate):
|
||||
return q in aggregate.name.lower()
|
||||
|
||||
return filter(comp, aggregates)
|
||||
|
||||
|
||||
class AvailabilityZoneFilterAction(tables.FilterAction):
|
||||
def filter(self, table, availability_zones, filter_string):
|
||||
q = filter_string.lower()
|
||||
|
||||
def comp(availabilityZone):
|
||||
return q in availabilityZone.name.lower()
|
||||
|
||||
return filter(comp, availability_zones)
|
||||
|
||||
|
||||
def get_aggregate_hosts(aggregate):
|
||||
return [host for host in aggregate.hosts]
|
||||
|
||||
|
||||
def get_available(zone):
|
||||
return zone.zoneState['available']
|
||||
|
||||
|
||||
def get_zone_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 HostAggregatesTable(tables.DataTable):
|
||||
name = tables.Column('name', verbose_name=_('Name'))
|
||||
availability_zone = tables.Column('availability_zone',
|
||||
verbose_name=_('Availability Zone'))
|
||||
hosts = tables.Column(get_aggregate_hosts,
|
||||
verbose_name=_("Hosts"),
|
||||
wrap_list=True,
|
||||
filters=(filters.unordered_list,))
|
||||
|
||||
class Meta:
|
||||
name = "host_aggregates"
|
||||
verbose_name = _("Host Aggregates")
|
||||
table_actions = (AggregateFilterAction,
|
||||
CreateAggregateAction,
|
||||
DeleteAggregateAction)
|
||||
row_actions = (UpdateAggregateAction,
|
||||
ManageHostsAction,
|
||||
DeleteAggregateAction)
|
||||
|
||||
|
||||
class AvailabilityZonesTable(tables.DataTable):
|
||||
name = tables.Column('zoneName',
|
||||
verbose_name=_('Availability Zone Name'))
|
||||
hosts = tables.Column(get_zone_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 = "availability_zones"
|
||||
verbose_name = _("Availability Zones")
|
||||
table_actions = (AggregateFilterAction,)
|
@ -0,0 +1,29 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:admin:aggregates:manage_hosts' id%}{% endblock %}
|
||||
|
||||
{% block modal_id %}add_aggregate_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Manage Hosts" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description" %}:</h3>
|
||||
<p>{% blocktrans %}
|
||||
Here you can add/remove hosts to the selected aggregate host.
|
||||
Note that while a host can be a member of multiple aggregates, it can belong to one availability zone at most.
|
||||
{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Add Host" %}" />
|
||||
<a href="{% url 'horizon:admin:aggregates:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,26 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}edit_aggregate_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:admin:aggregates:update' id %}{% endblock %}
|
||||
|
||||
{% block modal_id %}edit_aggregate_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Edit Host Aggregate" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description" %}:</h3>
|
||||
<p>{% trans "From here you can edit the aggregate name and availability zone" %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Save" %}" />
|
||||
<a href="{% url 'horizon:admin:aggregates:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create Host Aggregate" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Create Host Aggregate") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'horizon/common/_workflow.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,17 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Host Aggregates" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Host Aggregates") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
<div id="host-aggregates">
|
||||
{{ host_aggregates_table.render }}
|
||||
</div>
|
||||
|
||||
<div id="availability-zones">
|
||||
{{ availability_zones_table.render }}
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Manage Hosts Aggregate" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Manage Hosts Aggregate") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'admin/aggregates/_manage_hosts.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Edit Host Aggregate" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Edit Host Aggregate") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
|
||||
{% block main %}
|
||||
{% include 'admin/aggregates/_update.html' %}
|
||||
{% endblock %}
|
256
openstack_dashboard/dashboards/admin/aggregates/tests.py
Normal file
256
openstack_dashboard/dashboards/admin/aggregates/tests.py
Normal file
@ -0,0 +1,256 @@
|
||||
# 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 # noqa
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.admin.aggregates import constants
|
||||
from openstack_dashboard.dashboards.admin.aggregates import workflows
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
|
||||
class BaseAggregateWorkflowTests(test.BaseAdminViewTests):
|
||||
|
||||
def _get_create_workflow_data(self, aggregate, hosts=None):
|
||||
aggregate_info = {"name": aggregate.name,
|
||||
"availability_zone": aggregate.availability_zone}
|
||||
|
||||
if hosts:
|
||||
compute_hosts = []
|
||||
for host in hosts:
|
||||
if host.service == 'compute':
|
||||
compute_hosts.append(host)
|
||||
|
||||
host_field_name = 'add_host_to_aggregate_role_member'
|
||||
aggregate_info[host_field_name] = \
|
||||
[h.host_name for h in compute_hosts]
|
||||
|
||||
return aggregate_info
|
||||
|
||||
def _get_manage_workflow_data(self, aggregate, hosts=None, ):
|
||||
aggregate_info = {"id": aggregate.id}
|
||||
|
||||
if hosts:
|
||||
compute_hosts = []
|
||||
for host in hosts:
|
||||
if host.service == 'compute':
|
||||
compute_hosts.append(host)
|
||||
|
||||
host_field_name = 'add_host_to_aggregate_role_member'
|
||||
aggregate_info[host_field_name] = \
|
||||
[h.host_name for h in compute_hosts]
|
||||
|
||||
return aggregate_info
|
||||
|
||||
|
||||
class CreateAggregateWorkflowTests(BaseAggregateWorkflowTests):
|
||||
|
||||
@test.create_stubs({api.nova: ('host_list', ), })
|
||||
def test_workflow_get(self):
|
||||
|
||||
api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list())
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse(constants.AGGREGATES_CREATE_URL)
|
||||
res = self.client.get(url)
|
||||
workflow = res.context['workflow']
|
||||
|
||||
self.assertTemplateUsed(res, constants.AGGREGATES_CREATE_VIEW_TEMPLATE)
|
||||
self.assertEqual(workflow.name, workflows.CreateAggregateWorkflow.name)
|
||||
self.assertQuerysetEqual(workflow.steps,
|
||||
['<SetAggregateInfoStep: set_aggregate_info>',
|
||||
'<AddHostsToAggregateStep: add_host_to_aggregate>'])
|
||||
|
||||
@test.create_stubs({api.nova: ('host_list', 'aggregate_details_list',
|
||||
'aggregate_create'), })
|
||||
def test_create_aggregate(self):
|
||||
|
||||
aggregate = self.aggregates.first()
|
||||
|
||||
api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list())
|
||||
api.nova.aggregate_details_list(IsA(http.HttpRequest)).AndReturn([])
|
||||
|
||||
workflow_data = self._get_create_workflow_data(aggregate)
|
||||
api.nova.aggregate_create(IsA(http.HttpRequest),
|
||||
name=workflow_data['name'],
|
||||
availability_zone=
|
||||
workflow_data['availability_zone'])\
|
||||
.AndReturn(aggregate)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse(constants.AGGREGATES_CREATE_URL)
|
||||
res = self.client.post(url, workflow_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res,
|
||||
reverse(constants.AGGREGATES_INDEX_URL))
|
||||
|
||||
@test.create_stubs({api.nova: ('host_list',
|
||||
'aggregate_details_list',
|
||||
'aggregate_create',
|
||||
'add_host_to_aggregate'), })
|
||||
def test_create_aggregate_with_hosts(self):
|
||||
|
||||
aggregate = self.aggregates.first()
|
||||
hosts = self.hosts.list()
|
||||
|
||||
api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list())
|
||||
api.nova.aggregate_details_list(IsA(http.HttpRequest)).AndReturn([])
|
||||
|
||||
workflow_data = self._get_create_workflow_data(aggregate, hosts)
|
||||
api.nova.aggregate_create(IsA(http.HttpRequest),
|
||||
name=workflow_data['name'],
|
||||
availability_zone=
|
||||
workflow_data['availability_zone'])\
|
||||
.AndReturn(aggregate)
|
||||
|
||||
compute_hosts = []
|
||||
for host in hosts:
|
||||
if host.service == 'compute':
|
||||
compute_hosts.append(host)
|
||||
|
||||
for host in compute_hosts:
|
||||
api.nova.add_host_to_aggregate(IsA(http.HttpRequest),
|
||||
aggregate.id, host.host_name)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse(constants.AGGREGATES_CREATE_URL)
|
||||
res = self.client.post(url, workflow_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res,
|
||||
reverse(constants.AGGREGATES_INDEX_URL))
|
||||
|
||||
@test.create_stubs({api.nova: ('host_list', 'aggregate_details_list', ), })
|
||||
def test_host_list_nova_compute(self):
|
||||
|
||||
hosts = self.hosts.list()
|
||||
compute_hosts = []
|
||||
|
||||
for host in hosts:
|
||||
if host.service == 'compute':
|
||||
compute_hosts.append(host)
|
||||
|
||||
api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse(constants.AGGREGATES_CREATE_URL)
|
||||
res = self.client.get(url)
|
||||
workflow = res.context['workflow']
|
||||
step = workflow.get_step("add_host_to_aggregate")
|
||||
field_name = step.get_member_field_name('member')
|
||||
self.assertEqual(len(step.action.fields[field_name].choices),
|
||||
len(compute_hosts))
|
||||
|
||||
|
||||
class AggregatesViewTests(test.BaseAdminViewTests):
|
||||
|
||||
@test.create_stubs({api.nova: ('aggregate_details_list',
|
||||
'availability_zone_list',), })
|
||||
def test_index(self):
|
||||
api.nova.aggregate_details_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.aggregates.list())
|
||||
api.nova.availability_zone_list(IsA(http.HttpRequest), detailed=True) \
|
||||
.AndReturn(self.availability_zones.list())
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse(constants.AGGREGATES_INDEX_URL))
|
||||
self.assertTemplateUsed(res, constants.AGGREGATES_INDEX_VIEW_TEMPLATE)
|
||||
self.assertItemsEqual(res.context['host_aggregates_table'].data,
|
||||
self.aggregates.list())
|
||||
self.assertItemsEqual(res.context['availability_zones_table'].data,
|
||||
self.availability_zones.list())
|
||||
|
||||
@test.create_stubs({api.nova: ('aggregate_update', 'aggregate_get',), })
|
||||
def _test_generic_update_aggregate(self, form_data, aggregate,
|
||||
error_count=0,
|
||||
expected_error_message=None):
|
||||
api.nova.aggregate_get(IsA(http.HttpRequest), str(aggregate.id))\
|
||||
.AndReturn(aggregate)
|
||||
if not expected_error_message:
|
||||
az = form_data['availability_zone']
|
||||
aggregate_data = {'name': form_data['name'],
|
||||
'availability_zone': az}
|
||||
api.nova.aggregate_update(IsA(http.HttpRequest), str(aggregate.id),
|
||||
aggregate_data)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(reverse(constants.AGGREGATES_UPDATE_URL,
|
||||
args=[aggregate.id]),
|
||||
form_data)
|
||||
|
||||
if not expected_error_message:
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res,
|
||||
reverse(constants.AGGREGATES_INDEX_URL))
|
||||
else:
|
||||
self.assertFormErrors(res, error_count, expected_error_message)
|
||||
|
||||
def test_update_aggregate(self):
|
||||
aggregate = self.aggregates.first()
|
||||
form_data = {'id': aggregate.id,
|
||||
'name': 'my_new_name',
|
||||
'availability_zone': 'my_new_zone'}
|
||||
|
||||
self._test_generic_update_aggregate(form_data, aggregate)
|
||||
|
||||
def test_update_aggregate_fails_missing_fields(self):
|
||||
aggregate = self.aggregates.first()
|
||||
form_data = {'id': aggregate.id}
|
||||
|
||||
self._test_generic_update_aggregate(form_data, aggregate, 1,
|
||||
u'This field is required')
|
||||
|
||||
|
||||
class ManageHostsTests(test.BaseAdminViewTests):
|
||||
|
||||
def test_manage_hosts(self):
|
||||
aggregate = self.aggregates.first()
|
||||
res = self.client.get(reverse(constants.AGGREGATES_MANAGE_HOSTS_URL,
|
||||
args=[aggregate.id]))
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTemplateUsed(res,
|
||||
constants.AGGREGATES_MANAGE_HOSTS_TEMPLATE)
|
||||
|
||||
@test.create_stubs({api.nova: ('aggregate_get', 'add_host_to_aggregate',
|
||||
'host_list')})
|
||||
def test_manage_hosts_update_empty_aggregate(self):
|
||||
aggregate = self.aggregates.first()
|
||||
aggregate.hosts = []
|
||||
host = self.hosts.get(service="compute")
|
||||
|
||||
form_data = {'manageaggregatehostsaction_role_member':
|
||||
[host.host_name]}
|
||||
|
||||
api.nova.aggregate_get(IsA(http.HttpRequest), str(aggregate.id)) \
|
||||
.AndReturn(aggregate)
|
||||
api.nova.host_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.hosts.list())
|
||||
api.nova.aggregate_get(IsA(http.HttpRequest), str(aggregate.id)) \
|
||||
.AndReturn(aggregate)
|
||||
api.nova.add_host_to_aggregate(IsA(http.HttpRequest),
|
||||
str(aggregate.id), host.host_name)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(reverse(constants.AGGREGATES_MANAGE_HOSTS_URL,
|
||||
args=[aggregate.id]),
|
||||
form_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res,
|
||||
reverse(constants.AGGREGATES_INDEX_URL))
|
29
openstack_dashboard/dashboards/admin/aggregates/urls.py
Normal file
29
openstack_dashboard/dashboards/admin/aggregates/urls.py
Normal file
@ -0,0 +1,29 @@
|
||||
# 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 import patterns # noqa
|
||||
from django.conf.urls import url # noqa
|
||||
|
||||
from openstack_dashboard.dashboards.admin.aggregates \
|
||||
import views
|
||||
|
||||
|
||||
urlpatterns = patterns('openstack_dashboard.dashboards.admin.aggregates.views',
|
||||
url(r'^$',
|
||||
views.IndexView.as_view(), name='index'),
|
||||
url(r'^create/$',
|
||||
views.CreateView.as_view(), name='create'),
|
||||
url(r'^(?P<id>[^/]+)/update/$',
|
||||
views.UpdateView.as_view(), name='update'),
|
||||
url(r'^(?P<id>[^/]+)/manage_hosts/$',
|
||||
views.ManageHostsView.as_view(), name='manage_hosts'),
|
||||
)
|
108
openstack_dashboard/dashboards/admin/aggregates/views.py
Normal file
108
openstack_dashboard/dashboards/admin/aggregates/views.py
Normal file
@ -0,0 +1,108 @@
|
||||
# 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_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
from horizon import workflows
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.admin.aggregates \
|
||||
import constants
|
||||
from openstack_dashboard.dashboards.admin.aggregates \
|
||||
import forms as aggregate_forms
|
||||
from openstack_dashboard.dashboards.admin.aggregates \
|
||||
import tables as project_tables
|
||||
from openstack_dashboard.dashboards.admin.aggregates \
|
||||
import workflows as aggregate_workflows
|
||||
|
||||
|
||||
INDEX_URL = constants.AGGREGATES_INDEX_URL
|
||||
|
||||
|
||||
class IndexView(tables.MultiTableView):
|
||||
table_classes = (project_tables.HostAggregatesTable,
|
||||
project_tables.AvailabilityZonesTable)
|
||||
template_name = constants.AGGREGATES_TEMPLATE_NAME
|
||||
|
||||
def get_host_aggregates_data(self):
|
||||
request = self.request
|
||||
aggregates = []
|
||||
try:
|
||||
aggregates = api.nova.aggregate_details_list(self.request)
|
||||
except Exception:
|
||||
exceptions.handle(request,
|
||||
_('Unable to retrieve host aggregates list.'))
|
||||
aggregates.sort(key=lambda aggregate: aggregate.name.lower())
|
||||
return aggregates
|
||||
|
||||
def get_availability_zones_data(self):
|
||||
request = self.request
|
||||
availability_zones = []
|
||||
try:
|
||||
availability_zones = \
|
||||
api.nova.availability_zone_list(self.request, detailed=True)
|
||||
except Exception:
|
||||
exceptions.handle(request,
|
||||
_('Unable to retrieve availability zone list.'))
|
||||
availability_zones.sort(key=lambda az: az.zoneName.lower())
|
||||
return availability_zones
|
||||
|
||||
|
||||
class CreateView(workflows.WorkflowView):
|
||||
workflow_class = aggregate_workflows.CreateAggregateWorkflow
|
||||
template_name = constants.AGGREGATES_CREATE_VIEW_TEMPLATE
|
||||
|
||||
|
||||
class UpdateView(forms.ModalFormView):
|
||||
template_name = constants.AGGREGATES_UPDATE_VIEW_TEMPLATE
|
||||
form_class = aggregate_forms.UpdateAggregateForm
|
||||
success_url = reverse_lazy(constants.AGGREGATES_INDEX_URL)
|
||||
|
||||
def get_initial(self):
|
||||
aggregate = self.get_object()
|
||||
return {'id': self.kwargs["id"],
|
||||
'name': aggregate.name,
|
||||
'availability_zone': aggregate.availability_zone}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UpdateView, self).get_context_data(**kwargs)
|
||||
context['id'] = self.kwargs['id']
|
||||
return context
|
||||
|
||||
def get_object(self):
|
||||
if not hasattr(self, "_object"):
|
||||
aggregate_id = self.kwargs['id']
|
||||
try:
|
||||
self._object = \
|
||||
api.nova.aggregate_get(self.request, aggregate_id)
|
||||
except Exception:
|
||||
msg = _('Unable to retrieve the aggregate to be updated')
|
||||
exceptions.handle(self.request, msg)
|
||||
return self._object
|
||||
|
||||
|
||||
class ManageHostsView(workflows.WorkflowView):
|
||||
template_name = constants.AGGREGATES_MANAGE_HOSTS_TEMPLATE
|
||||
workflow_class = aggregate_workflows.ManageAggregateHostsWorkflow
|
||||
success_url = reverse_lazy(constants.AGGREGATES_INDEX_URL)
|
||||
|
||||
def get_initial(self):
|
||||
return {'id': self.kwargs["id"]}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ManageHostsView, self).get_context_data(**kwargs)
|
||||
context['id'] = self.kwargs['id']
|
||||
return context
|
238
openstack_dashboard/dashboards/admin/aggregates/workflows.py
Normal file
238
openstack_dashboard/dashboards/admin/aggregates/workflows.py
Normal file
@ -0,0 +1,238 @@
|
||||
# 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 _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import workflows
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.admin.aggregates import constants
|
||||
|
||||
|
||||
class SetAggregateInfoAction(workflows.Action):
|
||||
name = forms.CharField(label=_("Name"),
|
||||
max_length=255)
|
||||
|
||||
availability_zone = forms.CharField(label=_("Availability Zone"),
|
||||
max_length=255,
|
||||
required=False)
|
||||
|
||||
class Meta:
|
||||
name = _("Host Aggregate Info")
|
||||
help_text = _("From here you can create a new "
|
||||
"host aggregate to organize instances.")
|
||||
slug = "set_aggregate_info"
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(SetAggregateInfoAction, self).clean()
|
||||
name = cleaned_data.get('name')
|
||||
|
||||
try:
|
||||
aggregates = api.nova.aggregate_details_list(self.request)
|
||||
except Exception:
|
||||
msg = _('Unable to get host aggregate list')
|
||||
exceptions.check_message(["Connection", "refused"], msg)
|
||||
raise
|
||||
if aggregates is not None:
|
||||
for aggregate in aggregates:
|
||||
if aggregate.name.lower() == name.lower():
|
||||
raise forms.ValidationError(
|
||||
_('The name "%s" is already used by '
|
||||
'another host aggregate.')
|
||||
% name
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class SetAggregateInfoStep(workflows.Step):
|
||||
action_class = SetAggregateInfoAction
|
||||
contributes = ("availability_zone",
|
||||
"name")
|
||||
|
||||
|
||||
class AddHostsToAggregateAction(workflows.MembershipAction):
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(AddHostsToAggregateAction, self).__init__(request,
|
||||
*args,
|
||||
**kwargs)
|
||||
err_msg = _('Unable to get the available hosts')
|
||||
|
||||
default_role_field_name = self.get_default_role_field_name()
|
||||
self.fields[default_role_field_name] = forms.CharField(required=False)
|
||||
self.fields[default_role_field_name].initial = 'member'
|
||||
|
||||
field_name = self.get_member_field_name('member')
|
||||
self.fields[field_name] = forms.MultipleChoiceField(required=False)
|
||||
|
||||
hosts = []
|
||||
try:
|
||||
hosts = api.nova.host_list(request)
|
||||
except Exception:
|
||||
exceptions.handle(request, err_msg)
|
||||
|
||||
host_names = []
|
||||
for host in hosts:
|
||||
if host.host_name not in host_names and host.service == u'compute':
|
||||
host_names.append(host.host_name)
|
||||
host_names.sort()
|
||||
|
||||
self.fields[field_name].choices = \
|
||||
[(host_name, host_name) for host_name in host_names]
|
||||
|
||||
class Meta:
|
||||
name = _("Hosts within aggregate")
|
||||
slug = "add_host_to_aggregate"
|
||||
|
||||
|
||||
class ManageAggregateHostsAction(workflows.MembershipAction):
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(ManageAggregateHostsAction, self).__init__(request,
|
||||
*args,
|
||||
**kwargs)
|
||||
err_msg = _('Unable to get the available hosts')
|
||||
|
||||
default_role_field_name = self.get_default_role_field_name()
|
||||
self.fields[default_role_field_name] = forms.CharField(required=False)
|
||||
self.fields[default_role_field_name].initial = 'member'
|
||||
|
||||
field_name = self.get_member_field_name('member')
|
||||
self.fields[field_name] = forms.MultipleChoiceField(required=False)
|
||||
|
||||
aggregate_id = self.initial['id']
|
||||
aggregate = api.nova.aggregate_get(request, aggregate_id)
|
||||
aggregate_hosts = aggregate.hosts
|
||||
|
||||
hosts = []
|
||||
try:
|
||||
hosts = api.nova.host_list(request)
|
||||
except Exception:
|
||||
exceptions.handle(request, err_msg)
|
||||
|
||||
host_names = []
|
||||
for host in hosts:
|
||||
if host.host_name not in host_names and host.service == u'compute':
|
||||
host_names.append(host.host_name)
|
||||
host_names.sort()
|
||||
|
||||
self.fields[field_name].choices = \
|
||||
[(host_name, host_name) for host_name in host_names]
|
||||
|
||||
self.fields[field_name].initial = aggregate_hosts
|
||||
|
||||
class Meta:
|
||||
name = _("Hosts within aggregate")
|
||||
|
||||
|
||||
class AddHostsToAggregateStep(workflows.UpdateMembersStep):
|
||||
action_class = AddHostsToAggregateAction
|
||||
help_text = _("You can add hosts to this aggregate. One host can be added "
|
||||
"to one or more aggregate. You can also add the hosts later "
|
||||
"by editing the aggregate.")
|
||||
available_list_title = _("All available hosts")
|
||||
members_list_title = _("Selected hosts")
|
||||
no_available_text = _("No hosts found.")
|
||||
no_members_text = _("No host selected.")
|
||||
show_roles = False
|
||||
contributes = ("hosts_aggregate",)
|
||||
|
||||
def contribute(self, data, context):
|
||||
if data:
|
||||
member_field_name = self.get_member_field_name('member')
|
||||
context['hosts_aggregate'] = data.get(member_field_name, [])
|
||||
return context
|
||||
|
||||
|
||||
class ManageAggregateHostsStep(workflows.UpdateMembersStep):
|
||||
action_class = ManageAggregateHostsAction
|
||||
help_text = _("You can add hosts to this aggregate, as well as remove "
|
||||
"hosts from it.")
|
||||
available_list_title = _("All Available Hosts")
|
||||
members_list_title = _("Selected Hosts")
|
||||
no_available_text = _("No Hosts found.")
|
||||
no_members_text = _("No Host selected.")
|
||||
show_roles = False
|
||||
depends_on = ("id",)
|
||||
contributes = ("hosts_aggregate",)
|
||||
|
||||
def contribute(self, data, context):
|
||||
if data:
|
||||
member_field_name = self.get_member_field_name('member')
|
||||
context['hosts_aggregate'] = data.get(member_field_name, [])
|
||||
return context
|
||||
|
||||
|
||||
class CreateAggregateWorkflow(workflows.Workflow):
|
||||
slug = "create_aggregate"
|
||||
name = _("Create Host Aggregate")
|
||||
finalize_button_name = _("Create Host Aggregate")
|
||||
success_message = _('Created new host aggregate "%s".')
|
||||
failure_message = _('Unable to create host aggregate "%s".')
|
||||
success_url = constants.AGGREGATES_INDEX_URL
|
||||
default_steps = (SetAggregateInfoStep, AddHostsToAggregateStep)
|
||||
|
||||
def format_status_message(self, message):
|
||||
return message % self.context['name']
|
||||
|
||||
def handle(self, request, context):
|
||||
try:
|
||||
self.object = \
|
||||
api.nova.aggregate_create(
|
||||
request,
|
||||
name=context['name'],
|
||||
availability_zone=context['availability_zone'])
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Unable to create host aggregate.'))
|
||||
return False
|
||||
|
||||
hosts = context['hosts_aggregate']
|
||||
for host in hosts:
|
||||
try:
|
||||
api.nova.add_host_to_aggregate(request, self.object.id, host)
|
||||
except Exception:
|
||||
exceptions.handle(
|
||||
request, _('Error adding Hosts to the aggregate.'))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ManageAggregateHostsWorkflow(workflows.Workflow):
|
||||
slug = "manage_hosts_aggregate"
|
||||
name = _("Add/Remove Hosts to Aggregate")
|
||||
finalize_button_name = _("Save")
|
||||
success_message = _('The Aggregate was updated.')
|
||||
failure_message = _('Unable to update the aggregate.')
|
||||
success_url = constants.AGGREGATES_INDEX_URL
|
||||
default_steps = (ManageAggregateHostsStep, )
|
||||
|
||||
def format_status_message(self, message):
|
||||
return message
|
||||
|
||||
def handle(self, request, context):
|
||||
hosts_aggregate = context['hosts_aggregate']
|
||||
aggregate_id = context['id']
|
||||
aggregate = api.nova.aggregate_get(request, aggregate_id)
|
||||
aggregate_hosts = aggregate.hosts
|
||||
for host in aggregate_hosts:
|
||||
api.nova.remove_host_from_aggregate(request, aggregate_id, host)
|
||||
|
||||
for host in hosts_aggregate:
|
||||
try:
|
||||
api.nova.add_host_to_aggregate(request, aggregate_id, host)
|
||||
except Exception:
|
||||
exceptions.handle(
|
||||
request, _('Error updating the aggregate.'))
|
||||
return False
|
||||
|
||||
return True
|
@ -22,8 +22,9 @@ import horizon
|
||||
class SystemPanels(horizon.PanelGroup):
|
||||
slug = "admin"
|
||||
name = _("System Panel")
|
||||
panels = ('overview', 'metering', 'hypervisors', 'instances', 'volumes',
|
||||
'flavors', 'images', 'networks', 'routers', 'defaults', 'info')
|
||||
panels = ('overview', 'metering', 'hypervisors', 'aggregates',
|
||||
'instances', 'volumes', 'flavors', 'images',
|
||||
'networks', 'routers', 'defaults', 'info')
|
||||
|
||||
|
||||
class IdentityPanels(horizon.PanelGroup):
|
||||
|
17
openstack_dashboard/dashboards/admin/info/constants.py
Normal file
17
openstack_dashboard/dashboards/admin/info/constants.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Copyright 2014 Intel Corporation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
INFO_TEMPLATE_NAME = 'admin/info/index.html'
|
||||
INFO_DETAIL_TEMPLATE_NAME = 'horizon/common/_detail_table.html'
|
@ -66,37 +66,6 @@ def get_available(zone):
|
||||
return zone.zoneState['available']
|
||||
|
||||
|
||||
def get_zone_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_zone_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()
|
||||
@ -130,34 +99,6 @@ class NovaServicesTable(tables.DataTable):
|
||||
multi_select = False
|
||||
|
||||
|
||||
def get_aggregate_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_aggregate_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")
|
||||
|
||||
|
||||
class NetworkAgentsFilterAction(tables.FilterAction):
|
||||
def filter(self, table, agents, filter_string):
|
||||
q = filter_string.lower()
|
||||
|
@ -1,5 +1,3 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -24,6 +22,7 @@ from openstack_dashboard.api import keystone
|
||||
from openstack_dashboard.api import neutron
|
||||
from openstack_dashboard.api import nova
|
||||
|
||||
from openstack_dashboard.dashboards.admin.info import constants
|
||||
from openstack_dashboard.dashboards.admin.info import tables
|
||||
|
||||
|
||||
@ -31,7 +30,7 @@ class ServicesTab(tabs.TableTab):
|
||||
table_classes = (tables.ServicesTable,)
|
||||
name = _("Services")
|
||||
slug = "services"
|
||||
template_name = ("horizon/common/_detail_table.html")
|
||||
template_name = constants.INFO_DETAIL_TEMPLATE_NAME
|
||||
|
||||
def get_services_data(self):
|
||||
request = self.tab_group.request
|
||||
@ -43,50 +42,16 @@ class ServicesTab(tabs.TableTab):
|
||||
return services
|
||||
|
||||
|
||||
class ZonesTab(tabs.TableTab):
|
||||
table_classes = (tables.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 = (tables.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 = (tables.NovaServicesTable,)
|
||||
name = _("Compute Services")
|
||||
slug = "nova_services"
|
||||
template_name = ("horizon/common/_detail_table.html")
|
||||
template_name = constants.INFO_DETAIL_TEMPLATE_NAME
|
||||
|
||||
def get_nova_services_data(self):
|
||||
try:
|
||||
services = nova.service_list(self.tab_group.request)
|
||||
except Exception:
|
||||
services = []
|
||||
msg = _('Unable to get nova services list.')
|
||||
exceptions.check_message(["Connection", "refused"], msg)
|
||||
raise
|
||||
@ -98,7 +63,7 @@ class NetworkAgentsTab(tabs.TableTab):
|
||||
table_classes = (tables.NetworkAgentsTable,)
|
||||
name = _("Network Agents")
|
||||
slug = "network_agents"
|
||||
template_name = ("horizon/common/_detail_table.html")
|
||||
template_name = constants.INFO_DETAIL_TEMPLATE_NAME
|
||||
|
||||
def allowed(self, request):
|
||||
return base.is_service_enabled(request, 'network')
|
||||
@ -107,7 +72,6 @@ class NetworkAgentsTab(tabs.TableTab):
|
||||
try:
|
||||
agents = neutron.agent_list(self.tab_group.request)
|
||||
except Exception:
|
||||
agents = []
|
||||
msg = _('Unable to get network agents list.')
|
||||
exceptions.check_message(["Connection", "refused"], msg)
|
||||
raise
|
||||
@ -118,6 +82,5 @@ class NetworkAgentsTab(tabs.TableTab):
|
||||
class SystemInfoTabs(tabs.TabGroup):
|
||||
slug = "system_info"
|
||||
tabs = (ServicesTab, NovaServicesTab,
|
||||
ZonesTab, HostAggregatesTab,
|
||||
NetworkAgentsTab)
|
||||
sticky = True
|
||||
|
@ -26,17 +26,11 @@ INDEX_URL = reverse('horizon:admin:info:index')
|
||||
|
||||
class SystemInfoViewTests(test.BaseAdminViewTests):
|
||||
|
||||
@test.create_stubs({api.nova: ('service_list',
|
||||
'availability_zone_list',
|
||||
'aggregate_list'),
|
||||
@test.create_stubs({api.nova: ('service_list',),
|
||||
api.neutron: ('agent_list',)})
|
||||
def test_index(self):
|
||||
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())
|
||||
agents = self.agents.list()
|
||||
api.neutron.agent_list(IsA(http.HttpRequest)).AndReturn(agents)
|
||||
|
||||
@ -59,14 +53,6 @@ class SystemInfoViewTests(test.BaseAdminViewTests):
|
||||
'<Service: orchestration>',
|
||||
'<Service: database>'])
|
||||
|
||||
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>'])
|
||||
|
||||
network_agents_tab = res.context['tab_group'].get_tab('network_agents')
|
||||
self.assertQuerysetEqual(
|
||||
network_agents_tab._tables['network_agents'].data,
|
||||
|
@ -1,5 +1,3 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
@ -20,9 +18,10 @@
|
||||
|
||||
from horizon import tabs
|
||||
|
||||
from openstack_dashboard.dashboards.admin.info import constants
|
||||
from openstack_dashboard.dashboards.admin.info import tabs as project_tabs
|
||||
|
||||
|
||||
class IndexView(tabs.TabbedTableView):
|
||||
tab_group_class = project_tabs.SystemInfoTabs
|
||||
template_name = 'admin/info/index.html'
|
||||
template_name = constants.INFO_TEMPLATE_NAME
|
||||
|
@ -21,6 +21,7 @@ from novaclient.v1_1 import certs
|
||||
from novaclient.v1_1 import flavor_access
|
||||
from novaclient.v1_1 import flavors
|
||||
from novaclient.v1_1 import floating_ips
|
||||
from novaclient.v1_1 import hosts
|
||||
from novaclient.v1_1 import hypervisors
|
||||
from novaclient.v1_1 import keypairs
|
||||
from novaclient.v1_1 import quotas
|
||||
@ -170,6 +171,7 @@ def data(TEST):
|
||||
TEST.hypervisors = utils.TestDataContainer()
|
||||
TEST.services = utils.TestDataContainer()
|
||||
TEST.aggregates = utils.TestDataContainer()
|
||||
TEST.hosts = utils.TestDataContainer()
|
||||
|
||||
# Data return by novaclient.
|
||||
# It is used if API layer does data conversion.
|
||||
@ -620,7 +622,7 @@ def data(TEST):
|
||||
aggregate_1 = aggregates.Aggregate(aggregates.AggregateManager(None),
|
||||
{
|
||||
"name": "foo",
|
||||
"availability_zone": None,
|
||||
"availability_zone": "testing",
|
||||
"deleted": 0,
|
||||
"created_at": "2013-07-04T13:34:38.000000",
|
||||
"updated_at": None,
|
||||
@ -653,3 +655,22 @@ def data(TEST):
|
||||
|
||||
TEST.aggregates.add(aggregate_1)
|
||||
TEST.aggregates.add(aggregate_2)
|
||||
|
||||
host1 = hosts.Host(hosts.HostManager(None),
|
||||
{
|
||||
"host_name": "devstack001",
|
||||
"service": "compute",
|
||||
"zone": "testing"
|
||||
}
|
||||
)
|
||||
|
||||
host2 = hosts.Host(hosts.HostManager(None),
|
||||
{
|
||||
"host_name": "devstack002",
|
||||
"service": "nova-conductor",
|
||||
"zone": "testing"
|
||||
}
|
||||
)
|
||||
|
||||
TEST.hosts.add(host1)
|
||||
TEST.hosts.add(host2)
|
||||
|
Loading…
x
Reference in New Issue
Block a user