Host aggregates panel.
On this panel, aggregates could be added, deleted and edited. This patch takes the aggregates panel out of System Info and puts it back in the main admin panel list, as now, aggregates are not static information. The host can be associated to a host aggregate on this panel as well. Change-Id: I4ef2d87c33981db36d4ebd3de2f4841cdfa9dbfd Closes-Bug: #1261932 Implements: blueprint manage-host-aggregates Co-Authored-By: Santiago Baldassin <santiago.b.baldassin@intel.com> Co-Authored-By: Alejandro Paredes <alejandro.e.paredes@intel.com>
This commit is contained in:
parent
9f8a5eb349
commit
ed1525bc91
@ -690,15 +690,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.
|
||||
@ -616,7 +618,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,
|
||||
@ -649,3 +651,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…
Reference in New Issue
Block a user