diff --git a/doc/source/restapi/rest_api_v1.0.rst b/doc/source/restapi/rest_api_v1.0.rst index e17180f9..eaf54351 100644 --- a/doc/source/restapi/rest_api_v1.0.rst +++ b/doc/source/restapi/rest_api_v1.0.rst @@ -1283,6 +1283,8 @@ Also cluster scoped configurations can be defined in a Cluster Template. +-----------------+-------------------------------------------------------------------+-------------------------------------------------------+ | DELETE | /v1.0/{tenant_id}/cluster-templates/ | Deletes an existing Cluster Template by id. | +-----------------+-------------------------------------------------------------------+-------------------------------------------------------+ +| PUT | /v1.0/{tenant_id}/cluster-templates/ | Updates an existing Cluster Template by id. | ++-----------------+-------------------------------------------------------------------+-------------------------------------------------------+ **Examples** @@ -1845,6 +1847,172 @@ This operation does not require a request body. HTTP/1.1 204 NO CONTENT Content-Type: application/json +5.5 Update Cluster Template +--------------------------- + +.. http:put:: /v1.0/{tenant_id}/cluster-templates/{cluster_template_id} + +Normal Response Code: 202 (ACCEPTED) + +Errors: none + +This operation returns the updated Cluster Template. + +**Example**: + **request** + + .. sourcecode:: http + + PUT http://sahara/v1.0/775181/cluster-templates/1beae95b-fd20-47c0-a745-5125dccbd560 + + .. sourcecode:: json + + { + "cluster_template": { + "neutron_management_network": "0b001fb7-b172-43f0-8c99-444672fd0513", + "description": null, + "cluster_configs": {}, + "created_at": "2014-08-28 20:00:40", + "default_image_id": null, + "updated_at": null, + "plugin_name": "vanilla", + "anti_affinity": [], + "tenant_id": "28a4d0e49b024dc0875ed6a862b129f0", + "node_groups": [ + { + "count": 3, + "name": "worker", + "volume_mount_prefix": "/volumes/disk", + "auto_security_group": null, + "created_at": "2014-08-28 20:00:40", + "updated_at": null, + "floating_ip_pool": "cdeaa720-5517-4878-860e-71a1926744aa", + "image_id": null, + "volumes_size": 0, + "node_processes": [ + "datanode", + "nodemanager" + ], + "node_group_template_id": "3b975888-42d4-43d3-be70-8e4401e3cb65", + "volumes_per_node": 0, + "node_configs": { + "HDFS": { + "DataNode Heap Size": 1024 + }, + "YARN": { + "NodeManager Heap Size": 2048 + } + }, + "security_groups": null, + "flavor_id": "3" + }, + { + "count": 1, + "name": "master", + "volume_mount_prefix": "/volumes/disk", + "auto_security_group": null, + "created_at": "2014-08-28 20:00:40", + "updated_at": null, + "floating_ip_pool": "cdeaa720-5517-4878-860e-71a1926744aa", + "image_id": null, + "volumes_size": 0, + "node_processes": [ + "namenode", + "resourcemanager", + "oozie", + "historyserver" + ], + "node_group_template_id": "208f2d53-69c3-48c3-9830-986db4c29c95", + "volumes_per_node": 0, + "node_configs": {}, + "security_groups": null, + "flavor_id": "3" + } + ], + "hadoop_version": "2.4.1", + "id": "1beae95b-fd20-47c0-a745-5125dccbd560", + "name": "cluster-template" + } + } + + **response** + + .. sourcecode:: http + + HTTP/1.1 202 ACCEPTED + Content-Type: application/json + + .. sourcecode:: json + + { + "cluster_template": { + "neutron_management_network": "0b001fb7-b172-43f0-8c99-444672fd0513", + "description": null, + "cluster_configs": {}, + "created_at": "2014-08-28 20:00:40", + "default_image_id": null, + "updated_at": "2015-02-26 14:50:32.354180", + "plugin_name": "vanilla", + "anti_affinity": [], + "tenant_id": "28a4d0e49b024dc0875ed6a862b129f0", + "node_groups": [ + { + "count": 3, + "name": "worker", + "volume_mount_prefix": "/volumes/disk", + "auto_security_group": null, + "created_at": "2014-08-28 20:00:40", + "updated_at": null, + "floating_ip_pool": "cdeaa720-5517-4878-860e-71a1926744aa", + "image_id": null, + "volumes_size": 0, + "node_processes": [ + "datanode", + "nodemanager" + ], + "node_group_template_id": "3b975888-42d4-43d3-be70-8e4401e3cb65", + "volumes_per_node": 0, + "node_configs": { + "HDFS": { + "DataNode Heap Size": 1024 + }, + "YARN": { + "NodeManager Heap Size": 2048 + } + }, + "security_groups": null, + "flavor_id": "3" + }, + { + "count": 1, + "name": "master", + "volume_mount_prefix": "/volumes/disk", + "auto_security_group": null, + "created_at": "2014-08-28 20:00:40", + "updated_at": null, + "floating_ip_pool": "cdeaa720-5517-4878-860e-71a1926744aa", + "image_id": null, + "volumes_size": 0, + "node_processes": [ + "namenode", + "resourcemanager", + "oozie", + "historyserver" + ], + "node_group_template_id": "208f2d53-69c3-48c3-9830-986db4c29c95", + "volumes_per_node": 0, + "node_configs": {}, + "security_groups": null, + "flavor_id": "3" + } + ], + "hadoop_version": "2.4.1", + "id": "1beae95b-fd20-47c0-a745-5125dccbd560", + "name": "updated-cluster-template-name" + } + } + + 6 Clusters ========== diff --git a/sahara/api/v10.py b/sahara/api/v10.py index 8cde307a..6a21af8b 100644 --- a/sahara/api/v10.py +++ b/sahara/api/v10.py @@ -16,7 +16,6 @@ from oslo_log import log as logging from sahara.api import acl -import sahara.api.base as b from sahara.service import api from sahara.service import validation as v from sahara.service.validations import cluster_templates as v_ct @@ -100,8 +99,11 @@ def cluster_templates_get(cluster_template_id): @rest.put('/cluster-templates/') @acl.enforce("cluster-templates:modify") @v.check_exists(api.get_cluster_template, 'cluster_template_id') +@v.validate(None, v_ct.check_cluster_template_update) def cluster_templates_update(cluster_template_id, data): - return b.not_implemented() + return u.render( + api.update_cluster_template( + cluster_template_id, data).to_wrapped_dict()) @rest.delete('/cluster-templates/') diff --git a/sahara/conductor/api.py b/sahara/conductor/api.py index edaf870e..66f7004a 100644 --- a/sahara/conductor/api.py +++ b/sahara/conductor/api.py @@ -187,6 +187,16 @@ class LocalApi(object): self._manager.cluster_template_destroy(context, _get_id(cluster_template)) + @r.wrap(r.ClusterTemplateResource) + def cluster_template_update(self, context, id, cluster_template): + """Update the cluster template or raise if it does not exist. + + :returns: the updated cluster template + """ + return self._manager.cluster_template_update(context, + id, + cluster_template) + # Node Group Template ops @r.wrap(r.NodeGroupTemplateResource) diff --git a/sahara/conductor/manager.py b/sahara/conductor/manager.py index 345e4b85..b1110090 100644 --- a/sahara/conductor/manager.py +++ b/sahara/conductor/manager.py @@ -251,6 +251,17 @@ class ConductorManager(db_base.Base): """Destroy the cluster_template or raise if it does not exist.""" self.db.cluster_template_destroy(context, cluster_template) + def cluster_template_update(self, context, id, values): + """Update a cluster_template from the values dictionary.""" + values = copy.deepcopy(values) + values = _apply_defaults(values, CLUSTER_DEFAULTS) + values['tenant_id'] = context.tenant_id + values['id'] = id + + values['node_groups'] = self._populate_node_groups(context, values) + + return self.db.cluster_template_update(context, values) + # Node Group Template ops def node_group_template_get(self, context, node_group_template): diff --git a/sahara/db/api.py b/sahara/db/api.py index 06a545f3..852d4674 100644 --- a/sahara/db/api.py +++ b/sahara/db/api.py @@ -214,6 +214,12 @@ def cluster_template_destroy(context, cluster_template): IMPL.cluster_template_destroy(context, cluster_template) +@to_dict +def cluster_template_update(context, values): + """Update a cluster_template from the values dictionary.""" + return IMPL.cluster_template_update(context, values) + + # Node Group Template ops @to_dict diff --git a/sahara/db/sqlalchemy/api.py b/sahara/db/sqlalchemy/api.py index 1fa7d04e..f935641b 100644 --- a/sahara/db/sqlalchemy/api.py +++ b/sahara/db/sqlalchemy/api.py @@ -495,6 +495,51 @@ def cluster_template_destroy(context, cluster_template_id): session.delete(cluster_template) +def cluster_template_update(context, values): + node_groups = values.pop("node_groups", []) + + session = get_session() + with session.begin(): + cluster_template_id = values['id'] + cluster_template = (_cluster_template_get( + context, session, cluster_template_id)) + if not cluster_template: + raise ex.NotFoundException( + cluster_template_id, + _("Cluster Template id '%s' not found!")) + + name = values.get('name') + if name: + same_name_tmpls = model_query( + m.ClusterTemplate, context).filter_by( + name=name).all() + if (len(same_name_tmpls) > 0 and + same_name_tmpls[0].id != cluster_template_id): + raise ex.DBDuplicateEntry( + _("Cluster Template can not be updated. " + "Another cluster template with name %s already exists.") + % name + ) + + if len(cluster_template.clusters) > 0: + raise ex.UpdateFailedException( + cluster_template_id, + _("Cluster Template id '%s' can not be updated. " + "It is referenced by at least one cluster.") + ) + cluster_template.update(values) + + model_query(m.TemplatesRelation, context).filter_by( + cluster_template_id=cluster_template_id).delete() + for ng in node_groups: + node_group = m.TemplatesRelation() + node_group.update(ng) + node_group.update({"cluster_template_id": cluster_template_id}) + node_group.save(session=session) + + return cluster_template + + # Node Group Template ops def _node_group_template_get(context, session, node_group_template_id): diff --git a/sahara/service/api.py b/sahara/service/api.py index c5cadf49..e16dbc66 100644 --- a/sahara/service/api.py +++ b/sahara/service/api.py @@ -149,6 +149,10 @@ def terminate_cluster_template(id): return conductor.cluster_template_destroy(context.ctx(), id) +def update_cluster_template(id, values): + return conductor.cluster_template_update(context.ctx(), id, values) + + # NodeGroupTemplate ops def get_node_group_templates(**kwargs): diff --git a/sahara/service/validations/cluster_templates.py b/sahara/service/validations/cluster_templates.py index f1ab94a0..23045740 100644 --- a/sahara/service/validations/cluster_templates.py +++ b/sahara/service/validations/cluster_templates.py @@ -137,3 +137,26 @@ def check_cluster_template_usage(cluster_template_id, **kwargs): _("Cluster template %(id)s in use by %(clusters)s") % {'id': cluster_template_id, 'clusters': ', '.join(users)}) + + +def check_cluster_template_update(data, **kwargs): + if data.get('plugin_name'): + b.check_plugin_name_exists(data['plugin_name']) + + if data.get('plugin_name') and data.get('hadoop_version'): + b.check_plugin_supports_version(data['plugin_name'], + data['hadoop_version']) + b.check_all_configurations(data) + + if data.get('default_image_id'): + b.check_image_registered(data['default_image_id']) + b.check_required_image_tags(data['plugin_name'], + data['hadoop_version'], + data['default_image_id']) + + if data.get('anti_affinity'): + b.check_node_processes(data['plugin_name'], data['hadoop_version'], + data['anti_affinity']) + + if data.get('neutron_management_network'): + b.check_network_exists(data['neutron_management_network']) diff --git a/sahara/tests/unit/conductor/manager/test_templates.py b/sahara/tests/unit/conductor/manager/test_templates.py index f4c1d56a..0c6334e8 100644 --- a/sahara/tests/unit/conductor/manager/test_templates.py +++ b/sahara/tests/unit/conductor/manager/test_templates.py @@ -13,12 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy import testtools from sahara.conductor import manager from sahara import context from sahara import exceptions as ex import sahara.tests.unit.conductor.base as test_base +import sahara.tests.unit.conductor.manager.test_clusters as cluster_tests SAMPLE_NGT = { @@ -266,3 +268,37 @@ class ClusterTemplates(test_base.ConductorManagerTestCase): # Invalid field lst = self.api.cluster_template_get_all(ctx, **{'badfield': 'junk'}) self.assertEqual(len(lst), 0) + + def test_clt_update(self): + ctx = context.ctx() + clt = self.api.cluster_template_create(ctx, SAMPLE_CLT) + clt_id = clt["id"] + + UPDATE_NAME = "UpdatedClusterTemplate" + update_values = {"name": UPDATE_NAME} + updated_clt = self.api.cluster_template_update(ctx, + clt_id, + update_values) + self.assertEqual(UPDATE_NAME, updated_clt["name"]) + + updated_clt = self.api.cluster_template_get(ctx, clt_id) + self.assertEqual(UPDATE_NAME, updated_clt["name"]) + + # check duplicate name handling + clt = self.api.cluster_template_create(ctx, SAMPLE_CLT) + clt_id = clt["id"] + with testtools.ExpectedException(ex.DBDuplicateEntry): + self.api.cluster_template_update(ctx, clt_id, update_values) + + with testtools.ExpectedException(ex.NotFoundException): + self.api.cluster_template_update(ctx, -1, update_values) + + # create a cluster and try updating the referenced cluster template + cluster_val = copy.deepcopy(cluster_tests.SAMPLE_CLUSTER) + cluster_val['name'] = "ClusterTempalteUpdateTestCluster" + cluster_val['cluster_template_id'] = clt['id'] + self.api.cluster_create(ctx, cluster_val) + update_values = {"name": "noUpdateInUseName"} + + with testtools.ExpectedException(ex.UpdateFailedException): + self.api.cluster_template_update(ctx, clt['id'], update_values)