Merge "Host aggregates panel."

This commit is contained in:
Jenkins 2014-03-05 19:10:29 +00:00 committed by Gerrit Code Review
commit 3a153c86af
23 changed files with 1036 additions and 123 deletions

View File

@ -702,15 +702,42 @@ def service_list(request):
return novaclient(request).services.list()
def aggregate_list(request):
def aggregate_details_list(request):
result = []
c = novaclient(request)
for aggregate in c.aggregates.list():
result.append(c.aggregates.get_details(aggregate.id))
return result
def aggregate_create(request, name, availability_zone=None):
return novaclient(request).aggregates.create(name, availability_zone)
def aggregate_delete(request, aggregate_id):
return novaclient(request).aggregates.delete(aggregate_id)
def aggregate_get(request, aggregate_id):
return novaclient(request).aggregates.get(aggregate_id)
def aggregate_update(request, aggregate_id, values):
return novaclient(request).aggregates.update(aggregate_id, values)
def host_list(request):
return novaclient(request).hosts.list()
def add_host_to_aggregate(request, aggregate_id, host):
return novaclient(request).aggregates.add_host(aggregate_id, host)
def remove_host_from_aggregate(request, aggregate_id, host):
return novaclient(request).aggregates.remove_host(aggregate_id, host)
@memoized
def list_extensions(request):
return nova_list_extensions.ListExtManager(novaclient(request)).show_all()

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.
@ -620,7 +622,7 @@ def data(TEST):
aggregate_1 = aggregates.Aggregate(aggregates.AggregateManager(None),
{
"name": "foo",
"availability_zone": None,
"availability_zone": "testing",
"deleted": 0,
"created_at": "2013-07-04T13:34:38.000000",
"updated_at": None,
@ -653,3 +655,22 @@ def data(TEST):
TEST.aggregates.add(aggregate_1)
TEST.aggregates.add(aggregate_2)
host1 = hosts.Host(hosts.HostManager(None),
{
"host_name": "devstack001",
"service": "compute",
"zone": "testing"
}
)
host2 = hosts.Host(hosts.HostManager(None),
{
"host_name": "devstack002",
"service": "nova-conductor",
"zone": "testing"
}
)
TEST.hosts.add(host1)
TEST.hosts.add(host2)