Merge "Add "null" for fields in cluster and node group template JSON schemas"

This commit is contained in:
Jenkins 2015-06-08 15:35:11 +00:00 committed by Gerrit Code Review
commit 244fd1d9e0
10 changed files with 234 additions and 72 deletions

View File

@ -261,7 +261,8 @@ class ConductorManager(db_base.Base):
values['tenant_id'] = context.tenant_id values['tenant_id'] = context.tenant_id
values['id'] = id values['id'] = id
values['node_groups'] = self._populate_node_groups(context, values) if 'node_groups' in values:
values['node_groups'] = self._populate_node_groups(context, values)
return self.db.cluster_template_update(context, values, ignore_default) return self.db.cluster_template_update(context, values, ignore_default)

View File

@ -441,7 +441,11 @@ def cluster_template_destroy(context, cluster_template_id,
def cluster_template_update(context, values, ignore_default=False): def cluster_template_update(context, values, ignore_default=False):
node_groups = values.pop("node_groups", []) explicit_node_groups = "node_groups" in values
if explicit_node_groups:
node_groups = values.pop("node_groups")
if node_groups is None:
node_groups = []
session = get_session() session = get_session()
cluster_template_id = values['id'] cluster_template_id = values['id']
@ -475,7 +479,7 @@ def cluster_template_update(context, values, ignore_default=False):
# If node_groups has not been specified, then we are # If node_groups has not been specified, then we are
# keeping the old ones so don't delete! # keeping the old ones so don't delete!
if node_groups: if explicit_node_groups:
model_query(m.TemplatesRelation, model_query(m.TemplatesRelation,
context, session=session).filter_by( context, session=session).filter_by(
cluster_template_id=cluster_template_id).delete() cluster_template_id=cluster_template_id).delete()

View File

@ -261,13 +261,7 @@ def substitute_config_values(configs, template, path):
for opt, value in six.iteritems(configs): for opt, value in six.iteritems(configs):
if opt in opt_names and opt in template: if opt in opt_names and opt in template:
if value is None: if value is None:
# TODO(tmckay): someday if we support 'null' in JSON template[opt] = None
# we should replace this value with None. json.load
# will replace 'null' with None, and sqlalchemy will
# accept None as a value for a nullable field.
del template[opt]
LOG.debug("No replacement value specified for {opt} in "
"{path}, removing".format(opt=opt, path=path))
else: else:
# Use args to allow for keyword arguments to format # Use args to allow for keyword arguments to format
args = {opt: value} args = {opt: value}

View File

@ -63,30 +63,30 @@ CLUSTER_TEMPLATE_SCHEMA = {
"type": "string", "type": "string",
}, },
"default_image_id": { "default_image_id": {
"type": "string", "type": ["string", "null"],
"format": "uuid", "format": "uuid",
}, },
"cluster_configs": { "cluster_configs": {
"type": "configs", "type": ["configs", "null"],
}, },
"node_groups": { "node_groups": {
"type": "array", "type": ["array", "null"],
"items": { "items": {
"oneOf": [_cluster_tmpl_ng_tmpl_schema, "oneOf": [_cluster_tmpl_ng_tmpl_schema,
_cluster_tmpl_ng_schema] _cluster_tmpl_ng_schema]
} }
}, },
"anti_affinity": { "anti_affinity": {
"type": "array", "type": ["array", "null"],
"items": { "items": {
"type": "string", "type": "string",
}, },
}, },
"description": { "description": {
"type": "string", "type": ["string", "null"],
}, },
"neutron_management_network": { "neutron_management_network": {
"type": "string", "type": ["string", "null"],
"format": "uuid" "format": "uuid"
}, },
}, },

View File

@ -41,53 +41,51 @@ NODE_GROUP_TEMPLATE_SCHEMA = {
"minItems": 1 "minItems": 1
}, },
"image_id": { "image_id": {
"type": "string", "type": ["string", "null"],
"format": "uuid", "format": "uuid",
}, },
"node_configs": { "node_configs": {
"type": "configs", "type": ["configs", "null"],
}, },
"volumes_per_node": { "volumes_per_node": {
"type": "integer", "type": "integer",
"minimum": 0, "minimum": 0,
}, },
"volumes_size": { "volumes_size": {
"type": "integer", "type": ["integer", "null"],
"minimum": 1, "minimum": 1,
}, },
"volume_type": { "volume_type": {
"type": "string" "type": ["string", "null"],
}, },
"volumes_availability_zone": { "volumes_availability_zone": {
"type": "string", "type": ["string", "null"],
}, },
"volume_mount_prefix": { "volume_mount_prefix": {
"type": "string", "type": ["string", "null"],
"format": "posix_path", "format": "posix_path",
}, },
"description": { "description": {
"type": "string", "type": ["string", "null"],
}, },
"floating_ip_pool": { "floating_ip_pool": {
"type": "string", "type": ["string", "null"],
}, },
"security_groups": { "security_groups": {
"type": "array", "type": ["array", "null"],
"items": { "items": {"type": "string"}
"type": "string",
},
}, },
"auto_security_group": { "auto_security_group": {
"type": "boolean" "type": ["boolean", "null"],
}, },
"availability_zone": { "availability_zone": {
"type": "string", "type": ["string", "null"],
}, },
"is_proxy_gateway": { "is_proxy_gateway": {
"type": "boolean" "type": ["boolean", "null"],
}, },
"volume_local_to_instance": { "volume_local_to_instance": {
"type": "boolean" "type": ["boolean", "null"]
}, },
}, },
"additionalProperties": False, "additionalProperties": False,

View File

@ -14,26 +14,28 @@
# limitations under the License. # limitations under the License.
import copy import copy
import uuid
import six
from sqlalchemy import exc as sa_ex from sqlalchemy import exc as sa_ex
import testtools import testtools
from sahara.conductor import manager from sahara.conductor import manager
from sahara import context from sahara import context
from sahara import exceptions as ex from sahara import exceptions as ex
from sahara.service.validations import cluster_template_schema as cl_schema
from sahara.service.validations import node_group_template_schema as ngt_schema
import sahara.tests.unit.conductor.base as test_base import sahara.tests.unit.conductor.base as test_base
import sahara.tests.unit.conductor.manager.test_clusters as cluster_tests import sahara.tests.unit.conductor.manager.test_clusters as cluster_tests
SAMPLE_NGT = { SAMPLE_NGT = {
"plugin_name": "test_plugin",
"flavor_id": "42",
"tenant_id": "tenant_1",
"hadoop_version": "test_version",
"name": "ngt_test", "name": "ngt_test",
"flavor_id": "42",
"plugin_name": "test_plugin",
"hadoop_version": "test_version",
"node_processes": ["p1", "p2"], "node_processes": ["p1", "p2"],
"floating_ip_pool": None, "image_id": str(uuid.uuid4()),
"availability_zone": None,
"node_configs": { "node_configs": {
"service_1": { "service_1": {
"config_1": "value_1" "config_1": "value_1"
@ -41,14 +43,26 @@ SAMPLE_NGT = {
"service_2": { "service_2": {
"config_1": "value_1" "config_1": "value_1"
} }
} },
"volumes_per_node": 1,
"volumes_size": 1,
"volume_type": "big",
"volumes_availability_zone": "here",
"volume_mount_prefix": "/tmp",
"description": "my template",
"floating_ip_pool": "public",
"security_groups": ["cat", "dog"],
"auto_security_group": False,
"availability_zone": "here",
"is_proxy_gateway": False,
"volume_local_to_instance": False
} }
SAMPLE_CLT = { SAMPLE_CLT = {
"plugin_name": "test_plugin",
"tenant_id": "tenant_1",
"hadoop_version": "test_version",
"name": "clt_test", "name": "clt_test",
"plugin_name": "test_plugin",
"hadoop_version": "test_version",
"default_image_id": str(uuid.uuid4()),
"cluster_configs": { "cluster_configs": {
"service_1": { "service_1": {
"config_1": "value_1" "config_1": "value_1"
@ -77,7 +91,10 @@ SAMPLE_CLT = {
"availability_zone": None, "availability_zone": None,
} }
] ],
"anti_affinity": ["datanode"],
"description": "my template",
"neutron_management_network": str(uuid.uuid4())
} }
@ -213,6 +230,28 @@ class NodeGroupTemplates(test_base.ConductorManagerTestCase):
ignore_default=True) ignore_default=True)
self.assertEqual(UPDATE_NAME, updated_ngt["name"]) self.assertEqual(UPDATE_NAME, updated_ngt["name"])
def test_ngt_update_with_nulls(self):
ctx = context.ctx()
ngt = self.api.node_group_template_create(ctx, SAMPLE_NGT)
ngt_id = ngt["id"]
updated_values = copy.deepcopy(SAMPLE_NGT)
for prop, value in six.iteritems(
ngt_schema.NODE_GROUP_TEMPLATE_SCHEMA["properties"]):
if type(value["type"]) is list and "null" in value["type"]:
updated_values[prop] = None
# Prove that we can call update on these fields with null values
# without an exception
self.api.node_group_template_update(ctx,
ngt_id,
updated_values)
updated_ngt = self.api.node_group_template_get(ctx, ngt_id)
for prop, value in six.iteritems(updated_values):
if value is None:
self.assertIsNone(updated_ngt[prop])
class ClusterTemplates(test_base.ConductorManagerTestCase): class ClusterTemplates(test_base.ConductorManagerTestCase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -383,3 +422,30 @@ class ClusterTemplates(test_base.ConductorManagerTestCase):
update_values, update_values,
ignore_default=True) ignore_default=True)
self.assertEqual(UPDATE_NAME, updated_clt["name"]) self.assertEqual(UPDATE_NAME, updated_clt["name"])
def test_clt_update_with_nulls(self):
ctx = context.ctx()
clt = self.api.cluster_template_create(ctx, SAMPLE_CLT)
clt_id = clt["id"]
updated_values = copy.deepcopy(SAMPLE_CLT)
for prop, value in six.iteritems(
cl_schema.CLUSTER_TEMPLATE_SCHEMA["properties"]):
if type(value["type"]) is list and "null" in value["type"]:
updated_values[prop] = None
# Prove that we can call update on these fields with null values
# without an exception
self.api.cluster_template_update(ctx,
clt_id,
updated_values)
updated_clt = self.api.cluster_template_get(ctx, clt_id)
for prop, value in six.iteritems(updated_values):
if value is None:
# Conductor populates node groups with [] when
# the value given is null
if prop == "node_groups":
self.assertEqual([], updated_clt[prop])
else:
self.assertIsNone(updated_clt[prop])

View File

@ -204,7 +204,7 @@ class TemplateUpdateTestCase(base.ConductorManagerTestCase):
"floating_ip_pool": None} "floating_ip_pool": None}
template_api.substitute_config_values(configs, ngt, "/path") template_api.substitute_config_values(configs, ngt, "/path")
self.assertEqual("2", ngt["flavor_id"]) self.assertEqual("2", ngt["flavor_id"])
self.assertNotIn("floating_ip_pool", ngt) self.assertIsNone(ngt["floating_ip_pool"])
def test_substitute_config_values_clt(self): def test_substitute_config_values_clt(self):
clt = copy.copy(c.SAMPLE_CLT) clt = copy.copy(c.SAMPLE_CLT)
@ -216,7 +216,7 @@ class TemplateUpdateTestCase(base.ConductorManagerTestCase):
"default_image_id": None} "default_image_id": None}
template_api.substitute_config_values(configs, clt, "/path") template_api.substitute_config_values(configs, clt, "/path")
self.assertEqual(netid, clt["neutron_management_network"]) self.assertEqual(netid, clt["neutron_management_network"])
self.assertNotIn("default_image_id", clt) self.assertIsNone(clt["default_image_id"])
def _write_files(self, tempdir, templates): def _write_files(self, tempdir, templates):
files = [] files = []

View File

@ -13,6 +13,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import uuid
import mock
from sahara.service import api from sahara.service import api
from sahara.service.validations import cluster_template_schema as ct_schema from sahara.service.validations import cluster_template_schema as ct_schema
from sahara.service.validations import cluster_templates as ct from sahara.service.validations import cluster_templates as ct
@ -194,12 +198,52 @@ class TestClusterTemplateCreateValidation(u.ValidationTestCase):
u"'hadoop_version' is a required property") u"'hadoop_version' is a required property")
) )
def test_cluster_template_create_v_right(self): @mock.patch("sahara.service.validations.base.check_all_configurations")
@mock.patch("sahara.service.validations.base.check_network_exists")
@mock.patch("sahara.service.validations.base.check_required_image_tags")
@mock.patch("sahara.service.validations.base.check_image_registered")
def test_cluster_template_create_v_right(self, image_reg, image_tags,
net_exists, all_configs):
self._assert_create_object_validation( self._assert_create_object_validation(
data={ data={
'name': 'testname', 'name': 'testname',
'plugin_name': 'vanilla', 'plugin_name': 'vanilla',
'hadoop_version': '1.2.1' 'hadoop_version': '1.2.1',
'default_image_id': str(uuid.uuid4()),
'cluster_configs': {
"service_1": {
"config_1": "value_1"
}
},
'node_groups': [
{
"node_group_template_id": "550e8400-e29b-41d4-a716-"
"446655440000",
"name": "charlie",
'count': 3
}
],
'anti_affinity': ['datanode'],
'description': 'my template',
'neutron_management_network': str(uuid.uuid4())
})
@mock.patch("sahara.service.validations.base.check_network_exists")
@mock.patch("sahara.service.validations.base.check_required_image_tags")
@mock.patch("sahara.service.validations.base.check_image_registered")
def test_cluster_template_create_v_right_nulls(self, image_reg, image_tags,
net_exists):
self._assert_create_object_validation(
data={
'name': 'testname',
'plugin_name': 'vanilla',
'hadoop_version': '1.2.1',
'default_image_id': None,
'cluster_configs': None,
'node_groups': None,
'anti_affinity': None,
'description': None,
'neutron_management_network': None
}) })
def test_cluster_template_create_v_plugin_name_exists(self): def test_cluster_template_create_v_plugin_name_exists(self):

View File

@ -13,6 +13,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import mock
from sahara.service import api from sahara.service import api
from sahara.service.validations import node_group_template_schema as ngt_schema from sahara.service.validations import node_group_template_schema as ngt_schema
from sahara.service.validations import node_group_templates as nt from sahara.service.validations import node_group_templates as nt
@ -106,7 +108,14 @@ class TestNGTemplateCreateValidation(u.ValidationTestCase):
"['wrong_process']") "['wrong_process']")
) )
def test_ng_template_create_v_right(self): @mock.patch(
"sahara.service.validations.base.check_volume_availability_zone_exist")
@mock.patch("sahara.service.validations.base.check_volume_type_exists")
@mock.patch(
"sahara.service.validations.base.check_availability_zone_exist")
@mock.patch("sahara.service.validations.base.check_security_groups_exist")
def test_ng_template_create_v_right(self,
s_groups, a_zone, v_type, v_a_zone):
self._assert_create_object_validation( self._assert_create_object_validation(
data={ data={
'name': 'a', 'name': 'a',
@ -118,16 +127,53 @@ class TestNGTemplateCreateValidation(u.ValidationTestCase):
'secondarynamenode', 'secondarynamenode',
'tasktracker', 'tasktracker',
'jobtracker'], 'jobtracker'],
'image_id': '550e8400-e29b-41d4-a716-446655440000',
'node_configs': { 'node_configs': {
'HDFS': { 'HDFS': {
u'hadoop.tmp.dir': '/temp/' u'hadoop.tmp.dir': '/temp/'
} }
}, },
'image_id': '550e8400-e29b-41d4-a716-446655440000',
'volumes_per_node': 2, 'volumes_per_node': 2,
'volumes_size': 10, 'volumes_size': 10,
'description': 'test node template', 'volume_type': 'fish',
'floating_ip_pool': 'd9a3bebc-f788-4b81-9a93-aa048022c1ca' 'volumes_availability_zone': 'ocean',
'volume_mount_prefix': '/tmp',
'description': "my node group",
'floating_ip_pool': 'd9a3bebc-f788-4b81-9a93-aa048022c1ca',
'security_groups': ['cat', 'dog'],
'auto_security_group': False,
'availability_zone': 'here',
'is_proxy_gateway': False,
'volume_local_to_instance': False
}
)
def test_ng_template_create_v_nulls(self):
self._assert_create_object_validation(
data={
'name': 'a',
'flavor_id': '42',
'plugin_name': 'vanilla',
'hadoop_version': '1.2.1',
'node_processes': ['namenode',
'datanode',
'secondarynamenode',
'tasktracker',
'jobtracker'],
'image_id': None,
'node_configs': None,
'volumes_size': None,
'volume_type': None,
'volumes_availability_zone': None,
'volume_mount_prefix': None,
'description': None,
'floating_ip_pool': None,
'security_groups': None,
'auto_security_group': None,
'availability_zone': None,
'is_proxy_gateway': None,
'volume_local_to_instance': None
} }
) )

View File

@ -30,10 +30,11 @@ from sahara.tests.unit import testutils as tu
m = {} m = {}
_types_checks = { _types_checks = {
"string": [1, (), {}], "string": [1, (), {}, True],
"integer": ["a", (), {}], "integer": ["a", (), {}, True],
"uuid": ["z550e8400-e29b-41d4-a716-446655440000", 1, "a", (), {}], "uuid": ["z550e8400-e29b-41d4-a716-446655440000", 1, "a", (), {}, True],
"array": [{}, 'a', 1] "array": [{}, 'a', 1, True],
"boolean": [1, 'a', (), {}]
} }
@ -332,26 +333,34 @@ class ValidationTestCase(base.SaharaTestCase):
u"'a-!' is not a 'valid_name_hostname'") u"'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): def _assert_types(self, default_data):
for p_name in self.scheme['properties']: for p_name in self.scheme['properties']:
prop = self.scheme['properties'][p_name] prop = self.scheme['properties'][p_name]
if prop["type"] in _types_checks: prop_types = prop["type"]
for type_ex in _types_checks[prop["type"]]: if type(prop_types) is not list:
data = default_data.copy() prop_types = [prop_types]
value = type_ex for prop_type in prop_types:
value_str = str(value) if prop_type in _types_checks:
if isinstance(value, str): for type_ex in _types_checks[prop_type]:
value_str = "'%s'" % value_str data = default_data.copy()
data.update({p_name: value}) value = type_ex
message = ("%s is not of type '%s'" % value_str = str(value)
(value_str, prop["type"])) if isinstance(value, str):
if "enum" in prop: value_str = "'%s'" % value_str
message = [message, "%s is not one of %s" % data.update({p_name: value})
(value_str, prop["enum"])] message = ("%s is not of type %s" %
self._assert_create_object_validation( (value_str,
data=data, self._prop_types_str(prop_types)))
bad_req_i=(1, 'VALIDATION_ERROR', message) if "enum" in prop:
) message = [message, "%s is not one of %s" %
(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): def _assert_cluster_configs_validation(self, require_image_id=False):
data = { data = {