diff --git a/etc/sahara/policy.json b/etc/sahara/policy.json index ecd763d1..e3d2d308 100644 --- a/etc/sahara/policy.json +++ b/etc/sahara/policy.json @@ -7,6 +7,7 @@ "data-processing:clusters:scale": "", "data-processing:clusters:get": "", "data-processing:clusters:delete": "", + "data-processing:clusters:modify": "", "data-processing:cluster-templates:get_all": "", "data-processing:cluster-templates:create": "", diff --git a/sahara/api/v10.py b/sahara/api/v10.py index 28ac9915..5cef2bad 100644 --- a/sahara/api/v10.py +++ b/sahara/api/v10.py @@ -22,6 +22,7 @@ from sahara.service.validations import cluster_template_schema as ct_schema from sahara.service.validations import cluster_templates as v_ct from sahara.service.validations import clusters as v_c from sahara.service.validations import clusters_scaling as v_c_s +from sahara.service.validations import clusters_schema as v_c_schema from sahara.service.validations import images as v_images from sahara.service.validations import node_group_template_schema as ngt_schema from sahara.service.validations import node_group_templates as v_ngt @@ -45,14 +46,15 @@ def clusters_list(): @rest.post('/clusters') @acl.enforce("data-processing:clusters:create") -@v.validate(v_c.CLUSTER_SCHEMA, v_c.check_cluster_create) +@v.validate(v_c_schema.CLUSTER_SCHEMA, v_c.check_cluster_create) def clusters_create(data): return u.render(api.create_cluster(data).to_wrapped_dict()) @rest.post('/clusters/multiple') @acl.enforce("data-processing:clusters:create") -@v.validate(v_c.MULTIPLE_CLUSTER_SCHEMA, v_c.check_multiple_clusters_create) +@v.validate( + v_c_schema.MULTIPLE_CLUSTER_SCHEMA, v_c.check_multiple_clusters_create) def clusters_create_multiple(data): return u.render(api.create_multiple_clusters(data)) @@ -60,7 +62,7 @@ def clusters_create_multiple(data): @rest.put('/clusters/') @acl.enforce("data-processing:clusters:scale") @v.check_exists(api.get_cluster, 'cluster_id') -@v.validate(v_c_s.CLUSTER_SCALING_SCHEMA, v_c_s.check_cluster_scaling) +@v.validate(v_c_schema.CLUSTER_SCALING_SCHEMA, v_c_s.check_cluster_scaling) def clusters_scale(cluster_id, data): return u.to_wrapped_dict(api.scale_cluster, cluster_id, data) @@ -74,6 +76,14 @@ def clusters_get(cluster_id): return u.to_wrapped_dict(api.get_cluster, cluster_id, show_events) +@rest.patch('/clusters/') +@acl.enforce("data-processing:clusters:modify") +@v.check_exists(api.get_cluster, 'cluster_id') +@v.validate(v_c_schema.CLUSTER_UPDATE_SCHEMA) +def clusters_update(cluster_id, data): + return u.to_wrapped_dict(api.update_cluster, cluster_id, data) + + @rest.delete('/clusters/') @acl.enforce("data-processing:clusters:delete") @v.check_exists(api.get_cluster, 'cluster_id') diff --git a/sahara/db/sqlalchemy/api.py b/sahara/db/sqlalchemy/api.py index 3e7f2c67..c1604dbe 100644 --- a/sahara/db/sqlalchemy/api.py +++ b/sahara/db/sqlalchemy/api.py @@ -235,12 +235,16 @@ def cluster_create(context, values): def cluster_update(context, cluster_id, values): session = get_session() - with session.begin(): - cluster = _cluster_get(context, session, cluster_id) - if cluster is None: - raise ex.NotFoundException(cluster_id, - _("Cluster id '%s' not found!")) - cluster.update(values) + try: + with session.begin(): + cluster = _cluster_get(context, session, cluster_id) + if cluster is None: + raise ex.NotFoundException(cluster_id, + _("Cluster id '%s' not found!")) + cluster.update(values) + except db_exc.DBDuplicateEntry as e: + raise ex.DBDuplicateEntry( + _("Duplicate entry for Cluster: %s") % e.columns) return cluster diff --git a/sahara/service/api.py b/sahara/service/api.py index 12d52585..b82d973f 100644 --- a/sahara/service/api.py +++ b/sahara/service/api.py @@ -168,8 +168,12 @@ def terminate_cluster(id): sender.notify(context.ctx(), cluster.id, cluster.name, cluster.status, "delete") -# ClusterTemplate ops +def update_cluster(id, values): + return conductor.cluster_update(context.ctx(), id, values) + + +# ClusterTemplate ops def get_cluster_templates(**kwargs): return conductor.cluster_template_get_all(context.ctx(), **kwargs) diff --git a/sahara/service/validations/clusters.py b/sahara/service/validations/clusters.py index 3a9abf4a..d8d98a69 100644 --- a/sahara/service/validations/clusters.py +++ b/sahara/service/validations/clusters.py @@ -13,46 +13,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import copy - from oslo_config import cfg import sahara.exceptions as ex from sahara.i18n import _ import sahara.service.api as api import sahara.service.validations.base as b -import sahara.service.validations.cluster_template_schema as ct_schema CONF = cfg.CONF -def _build_cluster_schema(): - cluster_schema = copy.deepcopy(ct_schema.CLUSTER_TEMPLATE_SCHEMA) - cluster_schema['properties'].update({ - "is_transient": { - "type": "boolean" - }, - "user_keypair_id": { - "type": "string", - "format": "valid_keypair_name", - }, - "cluster_template_id": { - "type": "string", - "format": "uuid", - }}) - return cluster_schema - - -CLUSTER_SCHEMA = _build_cluster_schema() -MULTIPLE_CLUSTER_SCHEMA = copy.deepcopy(CLUSTER_SCHEMA) -MULTIPLE_CLUSTER_SCHEMA['properties'].update({ - "count": { - "type": "integer" - }}) -MULTIPLE_CLUSTER_SCHEMA['required'].append('count') - - def check_cluster_create(data, **kwargs): b.check_cluster_unique_name(data['name']) _check_cluster_create(data) diff --git a/sahara/service/validations/clusters_scaling.py b/sahara/service/validations/clusters_scaling.py index 1bb5309a..a715fa48 100644 --- a/sahara/service/validations/clusters_scaling.py +++ b/sahara/service/validations/clusters_scaling.py @@ -13,61 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import copy - import sahara.exceptions as ex from sahara.i18n import _ import sahara.plugins.base as plugin_base import sahara.service.api as api import sahara.service.validations.base as b -import sahara.service.validations.cluster_template_schema as ct_schema from sahara.utils import cluster as c_u -def _build_node_groups_schema(): - schema = copy.deepcopy(ct_schema.CLUSTER_TEMPLATE_SCHEMA) - return schema['properties']['node_groups'] - - -CLUSTER_SCALING_SCHEMA = { - "type": "object", - "properties": { - "resize_node_groups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - }, - "count": { - "type": "integer", - "minimum": 0, - }, - }, - "additionalProperties": False, - "required": [ - "name", - "count", - ] - }, - "minItems": 1 - }, - "add_node_groups": _build_node_groups_schema(), - }, - "additionalProperties": False, - "anyOf": [ - { - "required": ["resize_node_groups"] - }, - { - "required": ["add_node_groups"] - } - ] - -} - - def check_cluster_scaling(data, cluster_id, **kwargs): cluster = api.get_cluster(id=cluster_id) if cluster is None: diff --git a/sahara/service/validations/clusters_schema.py b/sahara/service/validations/clusters_schema.py new file mode 100644 index 00000000..b0d7f46e --- /dev/null +++ b/sahara/service/validations/clusters_schema.py @@ -0,0 +1,105 @@ +# Copyright (c) 2015 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 copy + +import sahara.service.validations.cluster_template_schema as ct_schema + + +def _build_node_groups_schema(): + schema = copy.deepcopy(ct_schema.CLUSTER_TEMPLATE_SCHEMA) + return schema['properties']['node_groups'] + + +def _build_cluster_schema(): + cluster_schema = copy.deepcopy(ct_schema.CLUSTER_TEMPLATE_SCHEMA) + cluster_schema['properties'].update({ + "is_transient": { + "type": "boolean" + }, + "user_keypair_id": { + "type": "string", + "format": "valid_keypair_name", + }, + "cluster_template_id": { + "type": "string", + "format": "uuid", + }}) + return cluster_schema + + +CLUSTER_SCHEMA = _build_cluster_schema() + +MULTIPLE_CLUSTER_SCHEMA = copy.deepcopy(CLUSTER_SCHEMA) +MULTIPLE_CLUSTER_SCHEMA['properties'].update({ + "count": { + "type": "integer" + }}) +MULTIPLE_CLUSTER_SCHEMA['required'].append('count') + +CLUSTER_UPDATE_SCHEMA = { + "type": "object", + "properties": { + "description": { + "type": ["string", "null"] + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "format": "valid_name_hostname", + } + }, + "additionalProperties": False, + "required": [] +} + +CLUSTER_SCALING_SCHEMA = { + "type": "object", + "properties": { + "resize_node_groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + }, + "count": { + "type": "integer", + "minimum": 0, + }, + }, + "additionalProperties": False, + "required": [ + "name", + "count", + ] + }, + "minItems": 1 + }, + "add_node_groups": _build_node_groups_schema(), + }, + "additionalProperties": False, + "anyOf": [ + { + "required": ["resize_node_groups"] + }, + { + "required": ["add_node_groups"] + } + ] + +} diff --git a/sahara/tests/unit/service/test_api.py b/sahara/tests/unit/service/test_api.py index 1a992a3e..2895c166 100644 --- a/sahara/tests/unit/service/test_api.py +++ b/sahara/tests/unit/service/test_api.py @@ -270,6 +270,13 @@ class TestApi(base.SaharaWithDbTestCase): with testtools.ExpectedException(exc.QuotaException): api.scale_cluster(cluster.id, {}) + def test_cluster_update(self): + with mock.patch('sahara.service.quotas.check_cluster'): + cluster = api.create_cluster(SAMPLE_CLUSTER) + updated_cluster = api.update_cluster( + cluster.id, {'description': 'Cluster'}) + self.assertEqual('Cluster', updated_cluster.description) + def test_get_plugin(self): api.get_plugin('fake', '0.1') api.get_plugin('fake', '0.3') diff --git a/sahara/tests/unit/service/validation/test_cluster_create_validation.py b/sahara/tests/unit/service/validation/test_cluster_create_validation.py index d469c7a3..bb1f3d1e 100644 --- a/sahara/tests/unit/service/validation/test_cluster_create_validation.py +++ b/sahara/tests/unit/service/validation/test_cluster_create_validation.py @@ -21,6 +21,7 @@ from sahara import exceptions from sahara import main from sahara.service import api from sahara.service.validations import clusters as c +from sahara.service.validations import clusters_schema as c_schema from sahara.tests.unit import base from sahara.tests.unit.service.validation import utils as u @@ -29,7 +30,7 @@ class TestClusterCreateValidation(u.ValidationTestCase): def setUp(self): super(TestClusterCreateValidation, self).setUp() self._create_object_fun = c.check_cluster_create - self.scheme = c.CLUSTER_SCHEMA + self.scheme = c_schema.CLUSTER_SCHEMA api.plugin_base.setup_plugins() def test_cluster_create_v_plugin_vers(self): diff --git a/sahara/tests/unit/service/validation/test_cluster_scaling_validation.py b/sahara/tests/unit/service/validation/test_cluster_scaling_validation.py index a7f61d64..844cddb7 100644 --- a/sahara/tests/unit/service/validation/test_cluster_scaling_validation.py +++ b/sahara/tests/unit/service/validation/test_cluster_scaling_validation.py @@ -22,6 +22,7 @@ from sahara.plugins.vanilla import plugin from sahara.service import api import sahara.service.validation as v from sahara.service.validations import clusters_scaling as c_s +from sahara.service.validations import clusters_schema as c_schema from sahara.tests.unit.service.validation import utils as u from sahara.tests.unit import testutils as tu @@ -176,7 +177,7 @@ class TestScalingValidation(u.ValidationTestCase): m_func = mock.Mock() m_func.__name__ = "m_func" req_data.return_value = data - v.validate(c_s.CLUSTER_SCALING_SCHEMA, + v.validate(c_schema.CLUSTER_SCALING_SCHEMA, self._create_object_fun)(m_func)(data=data, cluster_id='42') diff --git a/sahara/tests/unit/service/validation/test_cluster_update_validation.py b/sahara/tests/unit/service/validation/test_cluster_update_validation.py new file mode 100644 index 00000000..bf85c9e2 --- /dev/null +++ b/sahara/tests/unit/service/validation/test_cluster_update_validation.py @@ -0,0 +1,58 @@ +# 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 mock + +from sahara.service import api +from sahara.service.validations import clusters_schema as c_schema +from sahara.tests.unit.service.validation import utils as u + + +class TestClusterUpdateValidation(u.ValidationTestCase): + def setUp(self): + super(TestClusterUpdateValidation, self).setUp() + self._create_object_fun = mock.Mock() + self.scheme = c_schema.CLUSTER_UPDATE_SCHEMA + api.plugin_base.setup_plugins() + + def test_cluster_update_types(self): + self._assert_types({ + 'name': 'cluster', + 'description': 'very big cluster' + }) + + def test_cluster_update_nothing_required(self): + self._assert_create_object_validation( + data={} + ) + + def test_cluster_update(self): + self._assert_create_object_validation( + data={ + 'name': 'cluster', + 'description': 'very big cluster' + } + ) + + self._assert_create_object_validation( + data={ + 'name': 'cluster', + 'id': '1' + }, + bad_req_i=(1, "VALIDATION_ERROR", + "Additional properties are not allowed " + "('id' was unexpected)") + )