diff --git a/savanna/api/v10.py b/savanna/api/v10.py index c04d8e43..784f9da6 100644 --- a/savanna/api/v10.py +++ b/savanna/api/v10.py @@ -35,7 +35,7 @@ def clusters_list(): @rest.post('/clusters') -@v.validate(v_c.cluster_schema, v_c.check_cluster_create) +@v.validate(v_c.CLUSTER_SCHEMA, v_c.check_cluster_create) def clusters_create(data): return u.render(api.create_cluster(data).wrapped_dict) @@ -68,7 +68,7 @@ def cluster_templates_list(): @rest.post('/cluster-templates') -@v.validate(v_ct.cluster_template_schema, v_ct.check_cluster_template_create) +@v.validate(v_ct.CLUSTER_TEMPLATE_SCHEMA, v_ct.check_cluster_template_create) def cluster_templates_create(data): return u.render(api.create_cluster_template(data).wrapped_dict) @@ -102,7 +102,7 @@ def node_group_templates_list(): @rest.post('/node-group-templates') -@v.validate(v_ngt.node_group_template_schema, +@v.validate(v_ngt.NODE_GROUP_TEMPLATE_SCHEMA, v_ngt.check_node_group_template_create) def node_group_templates_create(data): return u.render(api.create_node_group_template(data).wrapped_dict) diff --git a/savanna/exceptions.py b/savanna/exceptions.py index 30065ba7..40b8f91b 100644 --- a/savanna/exceptions.py +++ b/savanna/exceptions.py @@ -40,3 +40,23 @@ class NotFoundException(SavannaException): self.value = value if message: self.message = message % value + + +class NameAlreadyExistsException(SavannaException): + + message = "Name already exists" + + def __init__(self, message=None): + self.code = "NAME_ALREADY_EXISTS" + if message: + self.message = message + + +class InvalidException(SavannaException): + + message = "Invalid object reference" + + def __init__(self, message=None): + self.code = "INVALID_REFERENCE" + if message: + self.message = message diff --git a/savanna/service/validations/base.py b/savanna/service/validations/base.py new file mode 100644 index 00000000..25165a1b --- /dev/null +++ b/savanna/service/validations/base.py @@ -0,0 +1,93 @@ +# 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 novaclient.exceptions as nova_ex + +import savanna.exceptions as ex +import savanna.plugins.base as plugin_base +import savanna.service.api as api +import savanna.utils.openstack.nova as nova + + +# Common validation checks +def check_plugin_name_exists(name): + if name not in [p.name for p in api.get_plugins()]: + raise ex.InvalidException("Savanna doesn't contain plugin with name %s" + % name) + + +def check_plugin_supports_version(p_name, version): + if version not in plugin_base.PLUGINS.get_plugin(p_name).get_versions(): + raise ex.InvalidException("Requested plugin '%s' doesn't support" + " version '%s'" % (p_name, version)) + + +def check_image_exists(image_id): + try: + # TODO(aignatov): Check supported images by plugin instead of it + api.get_image(id=image_id) + except nova_ex.NotFound: + raise ex.InvalidException("Requested image '%s' not found" + % image_id) + + +def check_flavor_exists(flavor_id): + try: + nova.client().flavors.get(flavor_id) + except nova_ex.NotFound: + raise ex.InvalidException("Requested flavor '%s' not found" + % flavor_id) + + +def check_node_processes(plugin_name, version, node_processes): + if len(set(node_processes)) != len(node_processes): + raise ex.InvalidException("Duplicates in node processes " + "have been detected") + plugin_procesess = [] + for process in plugin_base.PLUGINS.get_plugin( + plugin_name).get_node_processes(version).values(): + plugin_procesess += process + + if not set(node_processes).issubset(set(plugin_procesess)): + raise ex.InvalidException("Plugin supports the following " + "node procesess: " % plugin_procesess) + + +# Cluster creation related checks +def check_cluster_unique_name(name): + if name in [cluster.name for cluster in api.get_clusters()]: + raise ex.NameAlreadyExistsException("Cluster with name '%s' already" + " exists" % name) + + +def check_keypair_exists(keypair): + try: + nova.client().keypairs.get(keypair) + except nova_ex.NotFound: + raise ex.InvalidException("Requested keypair '%s' not found" % keypair) + + +# Cluster templates creation related checks +def check_cluster_template_unique_name(name): + if name in [t.name for t in api.get_cluster_templates()]: + raise ex.NameAlreadyExistsException("Cluster template with name '%s'" + " already exists" % name) + + +# NodeGroup templates related checks +def check_node_group_template_unique_name(name): + if name in [t.name for t in api.get_node_group_templates()]: + raise ex.NameAlreadyExistsException("NodeGroup template with name '%s'" + " already exists" % name) diff --git a/savanna/service/validations/cluster_templates.py b/savanna/service/validations/cluster_templates.py index c64f5d52..5d22394b 100644 --- a/savanna/service/validations/cluster_templates.py +++ b/savanna/service/validations/cluster_templates.py @@ -13,10 +13,91 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy -cluster_template_schema = { +import savanna.service.validations.base as b +import savanna.service.validations.node_group_templates as ng_tml + + +def _build_ng_schema_for_cluster_tmpl(): + cl_tmpl_ng_schema = copy.deepcopy(ng_tml.NODE_GROUP_TEMPLATE_SCHEMA) + cl_tmpl_ng_schema['properties'].update({"count": {"type": "integer"}}) + cl_tmpl_ng_schema["required"] = ['name', 'flavor_id', + 'node_processes', 'count'] + del cl_tmpl_ng_schema['properties']['hadoop_version'] + del cl_tmpl_ng_schema['properties']['plugin_name'] + return cl_tmpl_ng_schema + + +_cluster_tmpl_ng_schema = _build_ng_schema_for_cluster_tmpl() + + +def _build_ng_tmpl_schema_for_cluster_template(): + cl_tmpl_ng_tmpl_schema = copy.deepcopy(_cluster_tmpl_ng_schema) + cl_tmpl_ng_tmpl_schema['properties'].update( + { + "node_group_template_id": { + "type": "string", + "format": "uuid", + } + }) + cl_tmpl_ng_tmpl_schema["required"] = ["node_group_template_id", + "name", "count"] + return cl_tmpl_ng_tmpl_schema + +_cluster_tmpl_ng_tmpl_schema = _build_ng_tmpl_schema_for_cluster_template() + + +CLUSTER_TEMPLATE_SCHEMA = { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "format": "valid_name", + }, + "plugin_name": { + "type": "string", + }, + "hadoop_version": { + "type": "string", + }, + "default_image_id": { + "type": "string", + "format": "uuid", + }, + "cluster_configs": { + "type": "configs", + }, + "node_groups": { + "type": "array", + "items": { + "oneOf": [_cluster_tmpl_ng_tmpl_schema, + _cluster_tmpl_ng_schema] + } + }, + "anti_affinity_group": { + "type": "string", + }, + "description": { + "type": "string", + }, + }, + "additionalProperties": False, + "required": [ + "name", + "plugin_name", + "hadoop_version", + ] } def check_cluster_template_create(data): - pass + b.check_cluster_template_unique_name(data['name']) + b.check_plugin_name_exists(data['plugin_name']) + b.check_plugin_supports_version(data['plugin_name'], + data['hadoop_version']) + + if data.get('default_image_id'): + b.check_image_exists(data['default_image_id']) diff --git a/savanna/service/validations/clusters.py b/savanna/service/validations/clusters.py index 7255097d..b6d4ed90 100644 --- a/savanna/service/validations/clusters.py +++ b/savanna/service/validations/clusters.py @@ -13,9 +13,37 @@ # See the License for the specific language governing permissions and # limitations under the License. -cluster_schema = { -} +import copy + +import savanna.service.validations.base as b +import savanna.service.validations.cluster_templates as cl_tmpl + + +def _build_cluster_schema(): + cluster_schema = copy.deepcopy(cl_tmpl.CLUSTER_TEMPLATE_SCHEMA) + cluster_schema['properties']['name']['format'] = "hostname" + cluster_schema['properties'].update({ + "user_keypair_id": { + "type": "string", + "format": "valid_name", + }, + "cluster_template_id": { + "type": "string", + "format": "uuid", + }}) + return cluster_schema + +CLUSTER_SCHEMA = _build_cluster_schema() def check_cluster_create(data): - pass + b.check_cluster_unique_name(data['name']) + b.check_plugin_name_exists(data['plugin_name']) + b.check_plugin_supports_version(data['plugin_name'], + data['hadoop_version']) + + if data.get('user_keypair_id'): + b.check_keypair_exists(data['user_keypair_id']) + + if data.get('default_image_id'): + b.check_image_exists(data['default_image_id']) diff --git a/savanna/service/validations/node_group_templates.py b/savanna/service/validations/node_group_templates.py index 0f54a49a..c7afb909 100644 --- a/savanna/service/validations/node_group_templates.py +++ b/savanna/service/validations/node_group_templates.py @@ -13,11 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -node_group_template_schema = { +import savanna.service.validations.base as b + +NODE_GROUP_TEMPLATE_SCHEMA = { "type": "object", "properties": { "name": { "type": "string", + "minLength": 1, + "maxLength": 50, + "format": "valid_name", }, "flavor_id": { 'type': 'flavor', @@ -32,7 +37,8 @@ node_group_template_schema = { "type": "array", "items": { "type": "string", - } + }, + "minItems": 1 }, "image_id": { "type": "string", @@ -68,4 +74,13 @@ node_group_template_schema = { def check_node_group_template_create(data): - pass + b.check_node_group_template_unique_name(data['name']) + b.check_plugin_name_exists(data['plugin_name']) + b.check_plugin_supports_version(data['plugin_name'], + data['hadoop_version']) + b.check_flavor_exists(data['flavor_id']) + b.check_node_processes(data['plugin_name'], data['hadoop_version'], + data['node_processes']) + + if data.get('image_id'): + b.check_image_exists(data['image_id']) diff --git a/savanna/utils/api_validator.py b/savanna/utils/api_validator.py index b4674939..c9acfcee 100644 --- a/savanna/utils/api_validator.py +++ b/savanna/utils/api_validator.py @@ -13,12 +13,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re + import jsonschema import six from savanna.openstack.common import uuidutils +@jsonschema.FormatChecker.cls_checks('valid_name') +def validate_name_format(entry): + res = re.match(r"^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-_]" + r"*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z]" + r"[A-Za-z0-9\-_]*[A-Za-z0-9])$", entry) + return res is not None + + @jsonschema.FormatChecker.cls_checks('uuid') def validate_uuid_format(entry): return uuidutils.is_uuid_like(entry)