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:
Facundo Farias 2014-02-07 18:05:27 -03:00 committed by Julie Pichon
parent 9f8a5eb349
commit ed1525bc91
23 changed files with 1036 additions and 123 deletions

View File

@ -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()

View 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'

View 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

View 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)

View 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,)

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View 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))

View 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'),
)

View 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

View 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

View File

@ -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):

View 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'

View File

@ -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()

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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)