![Radomir Dopieralski](/assets/img/avatar_default.png)
Currently error handling in Tuskar-UI involves a lot of repeated code, as the same pattern is used everywhere and the same API calls tend to be handled in the same way no matter where they appear. This patch introduces a decorator that can be used to add default error handling to all API calls that take a request as a parameter. That default error handling can be disabled or modified using the special parameters passed to the call, all of which start with "_error" to avoid conflicts. Change-Id: Ie441789c471deeb6d64267bf4424f5661ef073af
407 lines
15 KiB
Python
407 lines
15 KiB
Python
# -*- coding: utf8 -*-
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import collections
|
|
import contextlib
|
|
|
|
from django.core import urlresolvers
|
|
from mock import patch, call # noqa
|
|
|
|
from openstack_dashboard.test.test_data import utils
|
|
from tuskar_ui.test import helpers as test
|
|
from tuskar_ui.test.test_data import tuskar_data
|
|
|
|
|
|
INDEX_URL = urlresolvers.reverse(
|
|
'horizon:infrastructure:overcloud:index')
|
|
CREATE_URL = urlresolvers.reverse(
|
|
'horizon:infrastructure:overcloud:create')
|
|
DETAIL_URL = urlresolvers.reverse(
|
|
'horizon:infrastructure:overcloud:detail', args=(1,))
|
|
DETAIL_URL_CONFIGURATION_TAB = (DETAIL_URL +
|
|
"?tab=detail__configuration")
|
|
DETAIL_URL_LOG_TAB = (DETAIL_URL + "?tab=detail__log")
|
|
DELETE_URL = urlresolvers.reverse(
|
|
'horizon:infrastructure:overcloud:undeploy_confirmation', args=(1,))
|
|
TEST_DATA = utils.TestDataContainer()
|
|
tuskar_data.data(TEST_DATA)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _mock_overcloud(**kwargs):
|
|
oc = None
|
|
stack = TEST_DATA.heatclient_stacks.first()
|
|
params = {
|
|
'spec_set': [
|
|
'counts',
|
|
'create',
|
|
'dashboard_url',
|
|
'delete',
|
|
'get',
|
|
'get_the_overcloud',
|
|
'id',
|
|
'is_deployed',
|
|
'is_deploying',
|
|
'is_failed',
|
|
'resources',
|
|
'stack',
|
|
'stack_events',
|
|
'update',
|
|
],
|
|
'counts': [],
|
|
'create.side_effect': lambda *args, **kwargs: oc,
|
|
'dashboard_url': '',
|
|
'delete.return_value': None,
|
|
'get.side_effect': lambda *args, **kwargs: oc,
|
|
'get_the_overcloud.side_effect': lambda *args, **kwargs: oc,
|
|
'id': 1,
|
|
'is_deployed': True,
|
|
'is_deploying': False,
|
|
'is_failed': False,
|
|
'resources.return_value': [],
|
|
'stack_events': [],
|
|
'stack': stack,
|
|
'update.side_effect': lambda *args, **kwargs: oc,
|
|
}
|
|
params.update(kwargs)
|
|
with patch('tuskar_ui.api.Overcloud', **params) as Overcloud:
|
|
oc = Overcloud
|
|
yield Overcloud
|
|
|
|
|
|
class OvercloudTests(test.BaseAdminViewTests):
|
|
|
|
def test_index_overcloud_undeployed_get(self):
|
|
with patch('tuskar_ui.api.Overcloud.list', return_value=[]):
|
|
res = self.client.get(INDEX_URL)
|
|
|
|
self.assertRedirectsNoFollow(res, CREATE_URL)
|
|
|
|
def test_index_overcloud_deployed_stack_not_created(self):
|
|
with _mock_overcloud(is_deployed=False, stack=None) as Overcloud:
|
|
res = self.client.get(INDEX_URL)
|
|
request = Overcloud.get_the_overcloud.call_args_list[0][0][0]
|
|
self.assertListEqual(Overcloud.get_the_overcloud.call_args_list,
|
|
[call(request)])
|
|
self.assertRedirectsNoFollow(res, DETAIL_URL)
|
|
|
|
def test_index_overcloud_deployed(self):
|
|
with _mock_overcloud() as Overcloud:
|
|
res = self.client.get(INDEX_URL)
|
|
request = Overcloud.get_the_overcloud.call_args_list[0][0][0]
|
|
self.assertListEqual(Overcloud.get_the_overcloud.call_args_list,
|
|
[call(request)])
|
|
|
|
self.assertRedirectsNoFollow(res, DETAIL_URL)
|
|
|
|
def test_create_get(self):
|
|
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
|
|
with contextlib.nested(
|
|
patch('tuskar_ui.api.OvercloudRole', **{
|
|
'spec_set': ['list'],
|
|
'list.return_value': roles,
|
|
}),
|
|
patch('tuskar_ui.api.Node', **{
|
|
'spec_set': ['list'],
|
|
'list.return_value': [],
|
|
}),
|
|
patch('openstack_dashboard.api.nova', **{
|
|
'spec_set': ['flavor_list'],
|
|
'flavor_list.return_value': [],
|
|
}),
|
|
):
|
|
res = self.client.get(CREATE_URL)
|
|
self.assertTemplateUsed(
|
|
res, 'infrastructure/_fullscreen_workflow_base.html')
|
|
self.assertTemplateUsed(
|
|
res, 'infrastructure/overcloud/undeployed_overview.html')
|
|
|
|
def test_create_post(self):
|
|
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
|
|
flavor = TEST_DATA.novaclient_flavors.first()
|
|
old_flavor_id = roles[0].flavor_id
|
|
roles[0].flavor_id = flavor.id
|
|
data = {
|
|
'count__1__%s' % flavor.id: '1',
|
|
'count__2__': '0',
|
|
'count__3__': '0',
|
|
'count__4__': '0',
|
|
}
|
|
with contextlib.nested(
|
|
patch('tuskar_ui.api.OvercloudRole', **{
|
|
'spec_set': ['list'],
|
|
'list.return_value': roles,
|
|
}),
|
|
_mock_overcloud(),
|
|
patch('tuskar_ui.api.Node', **{
|
|
'spec_set': ['list'],
|
|
'list.return_value': [],
|
|
}),
|
|
patch('openstack_dashboard.api.nova', **{
|
|
'spec_set': ['flavor_list'],
|
|
'flavor_list.return_value': [flavor],
|
|
}),
|
|
) as (OvercloudRole, Overcloud, Node, nova):
|
|
res = self.client.post(CREATE_URL, data)
|
|
request = Overcloud.create.call_args_list[0][0][0]
|
|
self.assertListEqual(
|
|
Overcloud.create.call_args_list,
|
|
[
|
|
call(request, {
|
|
('1', flavor.id): 1,
|
|
('2', ''): 0,
|
|
('3', ''): 0,
|
|
('4', ''): 0,
|
|
}, {
|
|
'NeutronPublicInterfaceRawDevice': '',
|
|
'NovaComputeDriver': '',
|
|
'NeutronPassword': '',
|
|
'NeutronFlatNetworks': '',
|
|
'NeutronPublicInterfaceDefaultRoute': '',
|
|
'HeatPassword': '',
|
|
'notcomputeImage': '',
|
|
'NovaImage': '',
|
|
'SSLKey': '',
|
|
'KeyName': '',
|
|
'GlancePassword': '',
|
|
'CeilometerPassword': '',
|
|
'NtpServer': '',
|
|
'CinderPassword': '',
|
|
'ImageUpdatePolicy': '',
|
|
'NeutronPublicInterface': '',
|
|
'NovaPassword': '',
|
|
'AdminToken': '',
|
|
'SwiftHashSuffix': '',
|
|
'NeutronPublicInterfaceIP': '',
|
|
'NovaComputeLibvirtType': '',
|
|
'AdminPassword': '',
|
|
'CeilometerComputeAgent': '',
|
|
'NeutronBridgeMappings': '',
|
|
'SwiftPassword': '',
|
|
'CeilometerMeteringSecret': '',
|
|
'SSLCertificate': '',
|
|
'Flavor': '',
|
|
}),
|
|
])
|
|
roles[0].flavor_id = old_flavor_id
|
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
|
|
|
def test_create_post_invalid_flavor(self):
|
|
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
|
|
old_flavor_id = roles[0].flavor_id
|
|
roles[0].flavor_id = 'non-existing'
|
|
data = {
|
|
'count__1__%s' % roles[0].flavor_id: '1',
|
|
'count__2__': '0',
|
|
'count__3__': '0',
|
|
'count__4__': '0',
|
|
}
|
|
with contextlib.nested(
|
|
patch('tuskar_ui.api.OvercloudRole', **{
|
|
'spec_set': ['list'],
|
|
'list.return_value': roles,
|
|
}),
|
|
_mock_overcloud(),
|
|
patch('tuskar_ui.api.Node', **{
|
|
'spec_set': ['list'],
|
|
'list.return_value': [],
|
|
}),
|
|
patch('openstack_dashboard.api.nova', **{
|
|
'spec_set': ['flavor_list'],
|
|
'flavor_list.return_value': [],
|
|
}),
|
|
) as (OvercloudRole, Overcloud, Node, nova):
|
|
res = self.client.post(CREATE_URL, data)
|
|
self.assertFormErrors(res)
|
|
roles[0].flavor_id = old_flavor_id
|
|
|
|
def test_detail_get(self):
|
|
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
|
|
with contextlib.nested(
|
|
_mock_overcloud(),
|
|
patch('tuskar_ui.api.OvercloudRole', **{
|
|
'spec_set': ['list'],
|
|
'list.return_value': roles,
|
|
}),
|
|
) as (Overcloud, OvercloudRole):
|
|
res = self.client.get(DETAIL_URL)
|
|
|
|
self.assertTemplateUsed(
|
|
res, 'infrastructure/overcloud/detail.html')
|
|
self.assertTemplateNotUsed(
|
|
res, 'horizon/common/_detail_table.html')
|
|
self.assertTemplateUsed(
|
|
res, 'infrastructure/overcloud/_detail_overview.html')
|
|
|
|
def test_detail_get_configuration_tab(self):
|
|
with _mock_overcloud():
|
|
res = self.client.get(DETAIL_URL_CONFIGURATION_TAB)
|
|
|
|
self.assertTemplateUsed(
|
|
res, 'infrastructure/overcloud/detail.html')
|
|
self.assertTemplateNotUsed(
|
|
res, 'infrastructure/overcloud/_detail_overview.html')
|
|
self.assertTemplateUsed(
|
|
res, 'horizon/common/_detail_table.html')
|
|
|
|
def test_detail_get_log_tab(self):
|
|
with _mock_overcloud():
|
|
res = self.client.get(DETAIL_URL_LOG_TAB)
|
|
|
|
self.assertTemplateUsed(
|
|
res, 'infrastructure/overcloud/detail.html')
|
|
self.assertTemplateNotUsed(
|
|
res, 'infrastructure/overcloud/_detail_overview.html')
|
|
self.assertTemplateUsed(
|
|
res, 'horizon/common/_detail_table.html')
|
|
|
|
def test_delete_get(self):
|
|
res = self.client.get(DELETE_URL)
|
|
self.assertTemplateUsed(
|
|
res, 'infrastructure/overcloud/undeploy_confirmation.html')
|
|
|
|
def test_delete_post(self):
|
|
with _mock_overcloud():
|
|
res = self.client.post(DELETE_URL)
|
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
|
|
|
def test_scale_get(self):
|
|
oc = None
|
|
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
|
|
with contextlib.nested(
|
|
patch('tuskar_ui.api.OvercloudRole', **{
|
|
'spec_set': ['list'],
|
|
'list.return_value': roles,
|
|
}),
|
|
_mock_overcloud(counts=[{
|
|
"overcloud_role_id": role.id,
|
|
"num_nodes": 0,
|
|
} for role in roles]),
|
|
patch('openstack_dashboard.api.nova', **{
|
|
'spec_set': ['flavor_list'],
|
|
'flavor_list.return_value': [],
|
|
}),
|
|
) as (OvercloudRole, Overcloud, nova):
|
|
oc = Overcloud
|
|
url = urlresolvers.reverse(
|
|
'horizon:infrastructure:overcloud:scale', args=(oc.id,))
|
|
res = self.client.get(url)
|
|
self.assertTemplateUsed(
|
|
res, 'infrastructure/overcloud/scale_node_counts.html')
|
|
|
|
def test_scale_post(self):
|
|
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
|
|
flavor = TEST_DATA.novaclient_flavors.first()
|
|
old_flavor_id = roles[0].flavor_id
|
|
roles[0].flavor_id = flavor.id
|
|
data = {
|
|
'overcloud_id': '1',
|
|
'count__1__%s' % flavor.id: '1',
|
|
'count__2__': '0',
|
|
'count__3__': '0',
|
|
'count__4__': '0',
|
|
}
|
|
with contextlib.nested(
|
|
patch('tuskar_ui.api.OvercloudRole', **{
|
|
'spec_set': ['list'],
|
|
'list.return_value': roles,
|
|
}),
|
|
_mock_overcloud(counts=[{
|
|
"overcloud_role_id": role.id,
|
|
"num_nodes": 0,
|
|
} for role in roles]),
|
|
patch('openstack_dashboard.api.nova', **{
|
|
'spec_set': ['flavor_list'],
|
|
'flavor_list.return_value': [flavor],
|
|
}),
|
|
) as (OvercloudRole, Overcloud, nova):
|
|
url = urlresolvers.reverse(
|
|
'horizon:infrastructure:overcloud:scale', args=(Overcloud.id,))
|
|
res = self.client.post(url, data)
|
|
# TODO(rdopieralski) Check it when it's actually called.
|
|
#request = Overcloud.update.call_args_list[0][0][0]
|
|
#self.assertListEqual(
|
|
# Overcloud.update.call_args_list,
|
|
# [
|
|
# call(request, {
|
|
# ('1', flavor.id): 1,
|
|
# ('2', ''): 0,
|
|
# ('3', ''): 0,
|
|
# ('4', ''): 0,
|
|
# }),
|
|
# ])
|
|
roles[0].flavor_id = old_flavor_id
|
|
self.assertRedirectsNoFollow(res, DETAIL_URL)
|
|
|
|
def test_role_edit_get(self):
|
|
role = TEST_DATA.tuskarclient_overcloud_roles.first()
|
|
url = urlresolvers.reverse(
|
|
'horizon:infrastructure:overcloud:role_edit', args=(role.id,))
|
|
with contextlib.nested(
|
|
patch('tuskar_ui.api.OvercloudRole', **{
|
|
'spec_set': ['get'],
|
|
'get.return_value': role,
|
|
}),
|
|
patch('openstack_dashboard.api.nova', **{
|
|
'spec_set': ['flavor_list'],
|
|
'flavor_list.return_value': [],
|
|
}),
|
|
):
|
|
res = self.client.get(url)
|
|
self.assertTemplateUsed(
|
|
res, 'infrastructure/overcloud/role_edit.html')
|
|
self.assertTemplateUsed(
|
|
res, 'infrastructure/overcloud/_role_edit.html')
|
|
|
|
def test_role_edit_post(self):
|
|
role = None
|
|
Flavor = collections.namedtuple('Flavor', 'id name')
|
|
flavor = Flavor('xxx', 'Xxx')
|
|
with contextlib.nested(
|
|
patch('tuskar_ui.api.OvercloudRole', **{
|
|
'spec_set': [
|
|
'get',
|
|
'update',
|
|
'id',
|
|
'name',
|
|
'description',
|
|
'image_name',
|
|
'flavor_id',
|
|
],
|
|
'get.side_effect': lambda *args, **kwargs: role,
|
|
'name': 'Compute',
|
|
'description': '...',
|
|
'image_name': '',
|
|
'id': 1,
|
|
'flavor_id': '',
|
|
}),
|
|
patch('openstack_dashboard.api.nova', **{
|
|
'spec_set': ['flavor_list'],
|
|
'flavor_list.return_value': [flavor],
|
|
}),
|
|
) as (OvercloudRole, nova):
|
|
role = OvercloudRole
|
|
url = urlresolvers.reverse(
|
|
'horizon:infrastructure:overcloud:role_edit', args=(role.id,))
|
|
data = {
|
|
'id': str(role.id),
|
|
'flavor_id': flavor.id,
|
|
}
|
|
res = self.client.post(url, data)
|
|
request = OvercloudRole.update.call_args_list[0][0][0]
|
|
self.assertListEqual(
|
|
OvercloudRole.update.call_args_list,
|
|
[call(request, flavor_id=flavor.id)])
|
|
self.assertRedirectsNoFollow(res, CREATE_URL)
|