447 lines
15 KiB
Python
447 lines
15 KiB
Python
# Copyright (c) 2013 Mirantis Inc.
|
|
#
|
|
# 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 ast
|
|
import re
|
|
|
|
import mock
|
|
import novaclient.exceptions as nova_ex
|
|
import six
|
|
|
|
from sahara.conductor import resource as r
|
|
from sahara.plugins.fake import plugin
|
|
import sahara.service.validation as v
|
|
from sahara.tests.unit import base
|
|
from sahara.tests.unit import testutils as tu
|
|
from sahara.utils import cluster as c_u
|
|
|
|
m = {}
|
|
|
|
_types_checks = {
|
|
"string": [1, (), {}, True],
|
|
"integer": ["a", (), {}, True],
|
|
"uuid": ["z550e8400-e29b-41d4-a716-446655440000", 1, "a", (), {}, True],
|
|
"array": [{}, 'a', 1, True],
|
|
"boolean": [1, 'a', (), {}]
|
|
}
|
|
|
|
|
|
def _update_data(data, update):
|
|
data.update(update)
|
|
return data
|
|
|
|
|
|
def _get_plugins():
|
|
fake = plugin.FakePluginProvider
|
|
fake.name = 'fake'
|
|
return [fake]
|
|
|
|
|
|
def _get_plugin(name):
|
|
if name == 'fake':
|
|
fake = plugin.FakePluginProvider
|
|
fake.name = 'fake'
|
|
return fake
|
|
return None
|
|
|
|
|
|
def _get_keypair(name):
|
|
if name != "test_keypair":
|
|
raise nova_ex.NotFound("")
|
|
|
|
|
|
def _get_network(**kwargs):
|
|
if 'id' in kwargs and (
|
|
kwargs['id'] != "d9a3bebc-f788-4b81-9a93-aa048022c1ca"):
|
|
raise nova_ex.NotFound("")
|
|
return 'OK'
|
|
|
|
|
|
def _get_fl_ip_pool_list():
|
|
return [FakeNetwork("d9a3bebc-f788-4b81-9a93-aa048022c1ca")]
|
|
|
|
|
|
def _get_availability_zone_list(detailed=True):
|
|
return [FakeAvailabilityZone('nova')]
|
|
|
|
|
|
def _get_heat_stack_list(**kwargs):
|
|
if (kwargs.get('filters') and
|
|
kwargs.get('filters').get('name') == 'test-heat'):
|
|
return [FakeStack('test-heat')]
|
|
return []
|
|
|
|
|
|
class FakeStack(object):
|
|
def __init__(self, name):
|
|
self.stack_name = name
|
|
|
|
|
|
class FakeNetwork(object):
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
|
|
class FakeAvailabilityZone(object):
|
|
def __init__(self, name):
|
|
self.zoneName = name
|
|
|
|
|
|
class FakeFlavor(object):
|
|
def __init__(self, id):
|
|
self.id = id
|
|
|
|
|
|
class FakeSecurityGroup(object):
|
|
def __init__(self, id, name):
|
|
self.id = id
|
|
self.name = name
|
|
|
|
|
|
def _get_flavors_list():
|
|
return [FakeFlavor("42")]
|
|
|
|
|
|
def _get_security_groups_list():
|
|
return [FakeSecurityGroup("1", "default"),
|
|
FakeSecurityGroup("2", "group1"),
|
|
FakeSecurityGroup("3", "group2")]
|
|
|
|
|
|
def start_patch(patch_templates=True):
|
|
get_clusters_p = mock.patch("sahara.service.api.v10.get_clusters")
|
|
get_cluster_p = mock.patch("sahara.service.api.v10.get_cluster")
|
|
if patch_templates:
|
|
get_ng_templates_p = mock.patch(
|
|
"sahara.service.api.v10.get_node_group_templates")
|
|
get_ng_template_p = mock.patch(
|
|
"sahara.service.api.v10.get_node_group_template")
|
|
if patch_templates:
|
|
get_cl_templates_p = mock.patch(
|
|
"sahara.service.api.v10.get_cluster_templates")
|
|
get_cl_template_p = mock.patch(
|
|
"sahara.service.api.v10.get_cluster_template")
|
|
nova_p = mock.patch("sahara.utils.openstack.nova.client")
|
|
heat_p = mock.patch("sahara.utils.openstack.heat.client")
|
|
image_manager_p = mock.patch(
|
|
"sahara.utils.openstack.images.SaharaImageManager")
|
|
cinder_p = mock.patch("sahara.utils.openstack.cinder.client")
|
|
cinder_exists_p = mock.patch(
|
|
"sahara.utils.openstack.cinder.check_cinder_exists")
|
|
get_image_p = mock.patch("sahara.service.api.v10.get_image")
|
|
|
|
get_image = get_image_p.start()
|
|
get_clusters = get_clusters_p.start()
|
|
get_cluster = get_cluster_p.start()
|
|
if patch_templates:
|
|
get_ng_templates = get_ng_templates_p.start()
|
|
get_ng_template = get_ng_template_p.start()
|
|
if patch_templates:
|
|
get_cl_templates = get_cl_templates_p.start()
|
|
get_cl_template_p.start()
|
|
|
|
nova = nova_p.start()
|
|
|
|
if patch_templates:
|
|
get_cl_templates.return_value = []
|
|
|
|
nova().flavors.list.side_effect = _get_flavors_list
|
|
nova().security_groups.list.side_effect = _get_security_groups_list
|
|
nova().keypairs.get.side_effect = _get_keypair
|
|
nova().networks.find.side_effect = _get_network
|
|
nova().networks.find.__name__ = 'find'
|
|
nova().floating_ip_pools.list.side_effect = _get_fl_ip_pool_list
|
|
nova().availability_zones.list.side_effect = _get_availability_zone_list
|
|
|
|
heat = heat_p.start()
|
|
heat().stacks.list.side_effect = _get_heat_stack_list
|
|
|
|
image_manager = image_manager_p.start()
|
|
|
|
cinder = cinder_p.start()
|
|
cinder().availability_zones.list.side_effect = _get_availability_zone_list
|
|
|
|
cinder_exists = cinder_exists_p.start()
|
|
cinder_exists.return_value = True
|
|
|
|
class Image(object):
|
|
def __init__(self, name='test'):
|
|
self.name = name
|
|
|
|
@property
|
|
def id(self):
|
|
if self.name == 'test':
|
|
return '550e8400-e29b-41d4-a716-446655440000'
|
|
else:
|
|
return '813fe450-40d2-4acc-ade5-ea753a1bd5bc'
|
|
|
|
@property
|
|
def tags(self):
|
|
if self.name == 'test':
|
|
return ['fake', '0.1']
|
|
else:
|
|
return ['fake', 'wrong_tag']
|
|
|
|
def _get_image(id):
|
|
if id == '550e8400-e29b-41d4-a716-446655440000':
|
|
return Image()
|
|
else:
|
|
return Image('wrong_test')
|
|
|
|
get_image.side_effect = _get_image
|
|
image_manager().list_registered.return_value = [Image(),
|
|
Image(name='wrong_name')]
|
|
ng_dict = tu.make_ng_dict('ng', '42', ['namenode'], 1)
|
|
cluster = tu.create_cluster('test', 't', 'fake', '0.1', [ng_dict],
|
|
id=1, status=c_u.CLUSTER_STATUS_ACTIVE)
|
|
# stub clusters list
|
|
get_clusters.return_value = [cluster]
|
|
get_cluster.return_value = cluster
|
|
|
|
# stub node templates
|
|
if patch_templates:
|
|
ngt_dict = {'name': 'test', 'tenant_id': 't', 'flavor_id': '42',
|
|
'plugin_name': 'fake', 'hadoop_version': '0.1',
|
|
'id': '550e8400-e29b-41d4-a716-446655440000',
|
|
'node_processes': ['namenode']}
|
|
|
|
get_ng_templates.return_value = [r.NodeGroupTemplateResource(ngt_dict)]
|
|
|
|
ct_dict = {'name': 'test', 'tenant_id': 't',
|
|
'plugin_name': 'fake', 'hadoop_version': '0.1'}
|
|
|
|
get_cl_templates.return_value = [r.ClusterTemplateResource(ct_dict)]
|
|
|
|
def _get_ng_template(id):
|
|
for template in get_ng_templates():
|
|
if template.id == id:
|
|
return template
|
|
return None
|
|
|
|
if patch_templates:
|
|
get_ng_template.side_effect = _get_ng_template
|
|
# request data to validate
|
|
patchers = [get_clusters_p, get_cluster_p,
|
|
nova_p, get_image_p, heat_p, image_manager_p, cinder_p,
|
|
cinder_exists_p]
|
|
if patch_templates:
|
|
patchers.extend([get_ng_template_p, get_ng_templates_p,
|
|
get_cl_template_p, get_cl_templates_p])
|
|
return patchers
|
|
|
|
|
|
def stop_patch(patchers):
|
|
for patcher in reversed(patchers):
|
|
patcher.stop()
|
|
|
|
|
|
class ValidationTestCase(base.SaharaTestCase):
|
|
def setUp(self):
|
|
super(ValidationTestCase, self).setUp()
|
|
self._create_object_fun = None
|
|
self.scheme = None
|
|
self.override_config('plugins', ['fake'])
|
|
|
|
def tearDown(self):
|
|
self._create_object_fun = None
|
|
super(ValidationTestCase, self).tearDown()
|
|
|
|
def _assert_calls(self, mock, call_info):
|
|
if not call_info:
|
|
self.assertEqual(0, mock.call_count, "Unexpected call to %s: %s"
|
|
% (mock.name, str(mock.call_args)))
|
|
else:
|
|
self.assertEqual(call_info[0], mock.call_count)
|
|
self.assertEqual(call_info[1], mock.call_args[0][0].code)
|
|
possible_messages = ([call_info[2]] if isinstance(
|
|
call_info[2], six.string_types) else call_info[2])
|
|
match = False
|
|
check = mock.call_args[0][0].message
|
|
if check.find('Error ID:') != -1:
|
|
check = check.split('\n')[0]
|
|
for message in possible_messages:
|
|
if self._check_match(message, check):
|
|
match = True
|
|
break
|
|
if not match:
|
|
self.assertIn(check, possible_messages)
|
|
|
|
def _check_match(self, expected, actual):
|
|
d1, r1 = self._extract_printed_dict(expected)
|
|
d2, r2 = self._extract_printed_dict(actual)
|
|
|
|
# Note(slukjanov): regex needed because of different
|
|
# versions of jsonschema generate different
|
|
# messages.
|
|
return (r1 == r2 or re.match(r1, r2)) and (d1 == d2)
|
|
|
|
def _extract_printed_dict(self, s):
|
|
start = s.find('{')
|
|
if start == -1:
|
|
return None, s
|
|
|
|
end = s.rfind('}')
|
|
if end == -1:
|
|
return None, s
|
|
|
|
return ast.literal_eval(s[start:end+1]), s[0:start+1] + s[end]
|
|
|
|
@mock.patch("sahara.utils.api.request_data")
|
|
@mock.patch("sahara.utils.api.bad_request")
|
|
def _assert_create_object_validation(
|
|
self, bad_req=None, request_data=None,
|
|
data=None, bad_req_i=None):
|
|
|
|
request_data.return_value = data
|
|
# mock function that should be validated
|
|
patchers = start_patch()
|
|
m_func = mock.Mock()
|
|
m_func.__name__ = "m_func"
|
|
v.validate(self.scheme, self._create_object_fun)(m_func)(data=data)
|
|
|
|
self.assertEqual(1, request_data.call_count)
|
|
self._assert_calls(bad_req, bad_req_i)
|
|
stop_patch(patchers)
|
|
|
|
def _assert_valid_name_hostname_validation(self, data):
|
|
|
|
data.update({'name': None})
|
|
self._assert_create_object_validation(
|
|
data=data,
|
|
bad_req_i=(1, "VALIDATION_ERROR",
|
|
u"name: None is not of type 'string'")
|
|
)
|
|
data.update({'name': ""})
|
|
self._assert_create_object_validation(
|
|
data=data,
|
|
bad_req_i=(1, "VALIDATION_ERROR",
|
|
u"name: '' is too short")
|
|
)
|
|
data.update({'name': ('a' * 51)})
|
|
self._assert_create_object_validation(
|
|
data=data,
|
|
bad_req_i=(1, "VALIDATION_ERROR",
|
|
u"name: '%s' is too long" % ('a' * 51))
|
|
)
|
|
data.update({'name': 'a-!'})
|
|
self._assert_create_object_validation(
|
|
data=data,
|
|
bad_req_i=(1, "VALIDATION_ERROR",
|
|
u"name: 'a-!' is not a 'valid_name_hostname'")
|
|
)
|
|
|
|
def _prop_types_str(self, prop_types):
|
|
return ", ".join(["'%s'" % prop for prop in prop_types])
|
|
|
|
def _assert_types(self, default_data):
|
|
for p_name in self.scheme['properties']:
|
|
prop = self.scheme['properties'][p_name]
|
|
prop_types = prop["type"]
|
|
if type(prop_types) is not list:
|
|
prop_types = [prop_types]
|
|
for prop_type in prop_types:
|
|
if prop_type in _types_checks:
|
|
for type_ex in _types_checks[prop_type]:
|
|
data = default_data.copy()
|
|
value = type_ex
|
|
value_str = str(value)
|
|
if isinstance(value, str):
|
|
value_str = "'%s'" % value_str
|
|
data.update({p_name: value})
|
|
message = ("%s: %s is not of type %s" %
|
|
(p_name, value_str,
|
|
self._prop_types_str(prop_types)))
|
|
if "enum" in prop:
|
|
message = [message, "%s: %s is not one of %s" %
|
|
(p_name, value_str,
|
|
prop["enum"])]
|
|
self._assert_create_object_validation(
|
|
data=data,
|
|
bad_req_i=(1, 'VALIDATION_ERROR', message)
|
|
)
|
|
|
|
def _assert_cluster_configs_validation(self, require_image_id=False):
|
|
data = {
|
|
'name': 'test-cluster',
|
|
'plugin_name': 'fake',
|
|
'hadoop_version': '0.1',
|
|
'cluster_configs': {
|
|
'HDFS': {
|
|
u'mapreduce.task.tmp.dir': '/temp/'
|
|
}
|
|
},
|
|
'default_image_id': '550e8400-e29b-41d4-a716-446655440000'
|
|
}
|
|
if require_image_id:
|
|
data_without_image = data.copy()
|
|
data_without_image.pop('default_image_id')
|
|
self._assert_create_object_validation(
|
|
data=data_without_image,
|
|
bad_req_i=(1, 'NOT_FOUND',
|
|
"'default_image_id' field is not found")
|
|
)
|
|
self._assert_create_object_validation(
|
|
data=_update_data(data.copy(), {
|
|
'cluster_configs': {
|
|
'wrong_target': {
|
|
u'mapreduce.task.tmp.dir': '/temp/'
|
|
}
|
|
}}),
|
|
bad_req_i=(1, 'INVALID_REFERENCE',
|
|
"Plugin doesn't contain applicable "
|
|
"target 'wrong_target'")
|
|
)
|
|
self._assert_create_object_validation(
|
|
data=_update_data(data.copy(), {
|
|
'cluster_configs': {
|
|
'general': {
|
|
u's': '/temp/'
|
|
}
|
|
}
|
|
}),
|
|
bad_req_i=(1, 'INVALID_REFERENCE',
|
|
"Plugin's applicable target 'general' doesn't "
|
|
"contain config with name 's'")
|
|
)
|
|
|
|
def _assert_cluster_default_image_tags_validation(self):
|
|
data = {
|
|
'name': 'test-cluster',
|
|
'plugin_name': 'fake',
|
|
'hadoop_version': '0.1',
|
|
'default_image_id': '550e8400-e29b-41d4-a716-446655440000',
|
|
'domain_name': 'domain.org.'
|
|
}
|
|
self._assert_create_object_validation(data=data)
|
|
data = {
|
|
'name': 'test-cluster',
|
|
'plugin_name': 'fake',
|
|
'hadoop_version': '0.1',
|
|
'default_image_id': '813fe450-40d2-4acc-ade5-ea753a1bd5bc'
|
|
}
|
|
self._assert_create_object_validation(
|
|
data=data,
|
|
bad_req_i=(1, 'INVALID_REFERENCE',
|
|
"Requested image "
|
|
"'813fe450-40d2-4acc-ade5-ea753a1bd5bc' "
|
|
"doesn't contain required tags: "
|
|
"['0.1']"))
|
|
|
|
def assert_protected_resource_exception(self, ex):
|
|
self.assertIn("marked as protected", six.text_type(ex))
|
|
|
|
def assert_created_in_another_tenant_exception(self, ex):
|
|
self.assertIn("wasn't created in this tenant", six.text_type(ex))
|