Adding ability to edit cluster templates

Changes to allow the updating of existing cluster templates.

Change-Id: I040fdcad6e3bd5ba8e8f841e0de554d4c296ec3f
Partial-Implements:  bp support-template-editing
This commit is contained in:
Chad Roberts 2015-02-19 12:36:43 -05:00
parent 48e5a3ef8d
commit b64142ae3a
9 changed files with 307 additions and 2 deletions

View File

@ -1283,6 +1283,8 @@ Also cluster scoped configurations can be defined in a Cluster Template.
+-----------------+-------------------------------------------------------------------+-------------------------------------------------------+
| DELETE | /v1.0/{tenant_id}/cluster-templates/<cluster_template_id> | Deletes an existing Cluster Template by id. |
+-----------------+-------------------------------------------------------------------+-------------------------------------------------------+
| PUT | /v1.0/{tenant_id}/cluster-templates/<cluster_template_id> | 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
==========

View File

@ -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/<cluster_template_id>')
@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/<cluster_template_id>')

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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'])

View File

@ -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)