Merge "Add OS::Senlin::Cluster resource"
This commit is contained in:
commit
c26e928866
@ -31,6 +31,8 @@ class SenlinClientPlugin(client_plugin.ClientPlugin):
|
||||
'auth_url': con.auth_url,
|
||||
'project_id': con.tenant_id,
|
||||
'token': self.auth_token,
|
||||
'user_id': con.user_id,
|
||||
'auth_plugin': 'token',
|
||||
}
|
||||
return client.Client(self.VERSION, **args)
|
||||
|
||||
|
0
heat/engine/resources/openstack/senlin/__init__.py
Normal file
0
heat/engine/resources/openstack/senlin/__init__.py
Normal file
214
heat/engine/resources/openstack/senlin/cluster.py
Normal file
214
heat/engine/resources/openstack/senlin/cluster.py
Normal file
@ -0,0 +1,214 @@
|
||||
# Copyright 2015 IBM Corp.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from heat.common import exception
|
||||
from heat.common.i18n import _
|
||||
from heat.engine import attributes
|
||||
from heat.engine import constraints
|
||||
from heat.engine import properties
|
||||
from heat.engine import resource
|
||||
from heat.engine import support
|
||||
|
||||
|
||||
class Cluster(resource.Resource):
|
||||
"""A resource that creates a Senlin Cluster.
|
||||
|
||||
Cluster resource in senlin can create and manage objects of
|
||||
the same nature, e.g. Nova servers, Heat stacks, Cinder volumes, etc.
|
||||
The collection of these objects is referred to as a cluster.
|
||||
"""
|
||||
|
||||
support_status = support.SupportStatus(version='6.0.0')
|
||||
|
||||
default_client_name = 'senlin'
|
||||
|
||||
PROPERTIES = (
|
||||
NAME, PROFILE, DESIRED_CAPACITY, MIN_SIZE, MAX_SIZE,
|
||||
METADATA, TIMEOUT
|
||||
) = (
|
||||
'name', 'profile', 'desired_capacity', 'min_size', 'max_size',
|
||||
'metadata', 'timeout'
|
||||
)
|
||||
|
||||
ATTRIBUTES = (
|
||||
ATTR_NAME, ATTR_METADATA, ATTR_NODES, ATTR_DESIRED_CAPACITY,
|
||||
ATTR_MIN_SIZE, ATTR_MAX_SIZE,
|
||||
) = (
|
||||
"name", 'metadata', 'nodes', 'desired_capacity',
|
||||
'min_size', 'max_size'
|
||||
)
|
||||
|
||||
_CLUSTER_STATUS = (
|
||||
CLUSTER_INIT, CLUSTER_ACTIVE, CLUSTER_ERROR, CLUSTER_WARNING,
|
||||
CLUSTER_CREATING, CLUSTER_DELETING, CLUSTER_UPDATING
|
||||
) = (
|
||||
'INIT', 'ACTIVE', 'ERROR', 'WARNING',
|
||||
'CREATING', 'DELETING', 'UPDATING'
|
||||
)
|
||||
|
||||
properties_schema = {
|
||||
PROFILE: properties.Schema(
|
||||
properties.Schema.STRING,
|
||||
_('The name or id of the Senlin profile.'),
|
||||
required=True,
|
||||
constraints=[
|
||||
constraints.CustomConstraint('senlin.profile')
|
||||
]
|
||||
),
|
||||
NAME: properties.Schema(
|
||||
properties.Schema.STRING,
|
||||
_('Name of the cluster. By default, physical resource name '
|
||||
'is used.'),
|
||||
),
|
||||
DESIRED_CAPACITY: properties.Schema(
|
||||
properties.Schema.INTEGER,
|
||||
_('Desired initial number of resources in cluster.'),
|
||||
default=0
|
||||
),
|
||||
MIN_SIZE: properties.Schema(
|
||||
properties.Schema.INTEGER,
|
||||
_('Minimum number of resources in the cluster.'),
|
||||
default=0,
|
||||
constraints=[
|
||||
constraints.Range(min=0)
|
||||
]
|
||||
),
|
||||
MAX_SIZE: properties.Schema(
|
||||
properties.Schema.INTEGER,
|
||||
_('Maximum number of resources in the cluster. '
|
||||
'-1 means unlimited.'),
|
||||
default=-1,
|
||||
constraints=[
|
||||
constraints.Range(min=-1)
|
||||
]
|
||||
),
|
||||
METADATA: properties.Schema(
|
||||
properties.Schema.MAP,
|
||||
_('Metadata key-values defined for cluster.'),
|
||||
),
|
||||
TIMEOUT: properties.Schema(
|
||||
properties.Schema.INTEGER,
|
||||
_('The number of seconds to wait for the cluster actions.'),
|
||||
constraints=[
|
||||
constraints.Range(min=0)
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
attributes_schema = {
|
||||
ATTR_NAME: attributes.Schema(
|
||||
_("Cluster name."),
|
||||
type=attributes.Schema.STRING
|
||||
),
|
||||
ATTR_METADATA: attributes.Schema(
|
||||
_("Cluster metadata."),
|
||||
type=attributes.Schema.MAP
|
||||
),
|
||||
ATTR_DESIRED_CAPACITY: attributes.Schema(
|
||||
_("Desired capacity of the cluster."),
|
||||
type=attributes.Schema.INTEGER
|
||||
),
|
||||
ATTR_NODES: attributes.Schema(
|
||||
_("Nodes list in the cluster."),
|
||||
type=attributes.Schema.LIST
|
||||
),
|
||||
ATTR_MIN_SIZE: attributes.Schema(
|
||||
_("Min size of the cluster."),
|
||||
type=attributes.Schema.INTEGER
|
||||
),
|
||||
ATTR_MAX_SIZE: attributes.Schema(
|
||||
_("Max size of the cluster."),
|
||||
type=attributes.Schema.INTEGER
|
||||
),
|
||||
}
|
||||
|
||||
def handle_create(self):
|
||||
params = {
|
||||
'name': (self.properties[self.NAME] or
|
||||
self.physical_resource_name()),
|
||||
'profile_id': self.properties[self.PROFILE],
|
||||
'desired_capacity': self.properties[self.DESIRED_CAPACITY],
|
||||
'min_size': self.properties[self.MIN_SIZE],
|
||||
'max_size': self.properties[self.MAX_SIZE],
|
||||
'metadata': self.properties[self.METADATA],
|
||||
'timeout': self.properties[self.TIMEOUT]
|
||||
}
|
||||
cluster = self.client().create_cluster(**params)
|
||||
self.resource_id_set(cluster.id)
|
||||
return cluster.id
|
||||
|
||||
def check_create_complete(self, resource_id):
|
||||
cluster = self.client().get_cluster(resource_id)
|
||||
if cluster.status in [self.CLUSTER_ACTIVE, self.CLUSTER_WARNING]:
|
||||
return True
|
||||
elif cluster.status in [self.CLUSTER_INIT, self.CLUSTER_CREATING]:
|
||||
return False
|
||||
else:
|
||||
raise exception.ResourceInError(
|
||||
status_reason=cluster.status_reason,
|
||||
resource_status=cluster.status
|
||||
)
|
||||
|
||||
def handle_delete(self):
|
||||
if self.resource_id is not None:
|
||||
with self.client_plugin().ignore_not_found:
|
||||
self.client().delete_cluster(self.resource_id)
|
||||
return self.resource_id
|
||||
|
||||
def check_delete_complete(self, resource_id):
|
||||
if not resource_id:
|
||||
return True
|
||||
|
||||
try:
|
||||
self.client().get_cluster(self.resource_id)
|
||||
except Exception as ex:
|
||||
self.client_plugin().ignore_not_found(ex)
|
||||
return True
|
||||
return False
|
||||
|
||||
def validate(self):
|
||||
min_size = self.properties[self.MIN_SIZE]
|
||||
max_size = self.properties[self.MAX_SIZE]
|
||||
desired_capacity = self.properties[self.DESIRED_CAPACITY]
|
||||
|
||||
if max_size != -1 and max_size < min_size:
|
||||
msg = _("%(min_size)s can not be greater than %(max_size)s") % {
|
||||
'min_size': self.MIN_SIZE,
|
||||
'max_size': self.MAX_SIZE,
|
||||
}
|
||||
raise exception.StackValidationFailed(message=msg)
|
||||
|
||||
if (desired_capacity < min_size or
|
||||
(max_size != -1 and desired_capacity > max_size)):
|
||||
msg = _("%(desired_capacity)s must be between %(min_size)s "
|
||||
"and %(max_size)s") % {
|
||||
'desired_capacity': self.DESIRED_CAPACITY,
|
||||
'min_size': self.MIN_SIZE,
|
||||
'max_size': self.MAX_SIZE,
|
||||
}
|
||||
raise exception.StackValidationFailed(message=msg)
|
||||
|
||||
def _resolve_attribute(self, name):
|
||||
cluster = self.client().get_cluster(self.resource_id)
|
||||
return getattr(cluster, name, None)
|
||||
|
||||
def _show_resource(self):
|
||||
cluster = self.client().get_cluster(self.resource_id)
|
||||
return cluster.to_dict()
|
||||
|
||||
|
||||
def resource_mapping():
|
||||
return {
|
||||
'OS::Senlin::Cluster': Cluster
|
||||
}
|
0
heat/tests/openstack/senlin/__init__.py
Normal file
0
heat/tests/openstack/senlin/__init__.py
Normal file
199
heat/tests/openstack/senlin/test_cluster.py
Normal file
199
heat/tests/openstack/senlin/test_cluster.py
Normal file
@ -0,0 +1,199 @@
|
||||
# Copyright 2015 IBM Corp.
|
||||
#
|
||||
# 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 oslo_config import cfg
|
||||
import six
|
||||
|
||||
from heat.common import exception
|
||||
from heat.common import template_format
|
||||
from heat.engine.clients.os import senlin
|
||||
from heat.engine.resources.openstack.senlin import cluster as sc
|
||||
from heat.engine import scheduler
|
||||
from heat.tests import common
|
||||
from heat.tests import utils
|
||||
from senlinclient.common import exc
|
||||
|
||||
|
||||
cluster_stack_template = """
|
||||
heat_template_version: 2016-04-08
|
||||
description: Senlin Cluster Template
|
||||
resources:
|
||||
senlin-cluster:
|
||||
type: OS::Senlin::Cluster
|
||||
properties:
|
||||
name: SenlinCluster
|
||||
profile: fake_profile
|
||||
min_size: 0
|
||||
max_size: -1
|
||||
desired_capacity: 1
|
||||
timeout: 3600
|
||||
metadata:
|
||||
foo: bar
|
||||
"""
|
||||
|
||||
|
||||
class FakeCluster(object):
|
||||
def __init__(self, id='some_id', status='ACTIVE'):
|
||||
self.status = status
|
||||
self.status_reason = 'Unknown'
|
||||
self.id = id
|
||||
self.name = "SenlinCluster"
|
||||
self.metadata = {}
|
||||
self.nodes = ['node1']
|
||||
self.desired_capacity = 1
|
||||
self.metadata = {'foo': 'bar'}
|
||||
self.timeout = 3600
|
||||
self.max_size = -1
|
||||
self.min_size = 0
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'status': self.status,
|
||||
'status_reason': self.status_reason,
|
||||
'name': self.name,
|
||||
'metadata': self.metadata,
|
||||
'timeout': self.timeout,
|
||||
'desired_capacity': self.desired_capacity,
|
||||
'max_size': self.max_size,
|
||||
'min_size': self.min_size,
|
||||
'nodes': self.nodes
|
||||
}
|
||||
|
||||
|
||||
class SenlinClusterTest(common.HeatTestCase):
|
||||
def setUp(self):
|
||||
super(SenlinClusterTest, self).setUp()
|
||||
self.senlin_mock = mock.MagicMock()
|
||||
self.patchobject(sc.Cluster, 'client', return_value=self.senlin_mock)
|
||||
self.patchobject(senlin.ProfileConstraint, 'validate',
|
||||
return_value=True)
|
||||
self.fake_cl = FakeCluster()
|
||||
self.t = template_format.parse(cluster_stack_template)
|
||||
|
||||
def _init_cluster(self, template):
|
||||
self.stack = utils.parse_stack(template)
|
||||
cluster = self.stack['senlin-cluster']
|
||||
return cluster
|
||||
|
||||
def _create_cluster(self, template):
|
||||
cluster = self._init_cluster(template)
|
||||
self.senlin_mock.create_cluster.return_value = self.fake_cl
|
||||
self.senlin_mock.get_cluster.return_value = self.fake_cl
|
||||
scheduler.TaskRunner(cluster.create)()
|
||||
self.assertEqual((cluster.CREATE, cluster.COMPLETE),
|
||||
cluster.state)
|
||||
self.assertEqual(self.fake_cl.id, cluster.resource_id)
|
||||
return cluster
|
||||
|
||||
def test_resource_mapping(self):
|
||||
mapping = sc.resource_mapping()
|
||||
self.assertEqual(1, len(mapping))
|
||||
self.assertEqual(sc.Cluster,
|
||||
mapping['OS::Senlin::Cluster'])
|
||||
|
||||
def test_cluster_create_success(self):
|
||||
self._create_cluster(self.t)
|
||||
expect_kwargs = {
|
||||
'name': 'SenlinCluster',
|
||||
'profile_id': 'fake_profile',
|
||||
'desired_capacity': 1,
|
||||
'min_size': 0,
|
||||
'max_size': -1,
|
||||
'metadata': {'foo': 'bar'},
|
||||
'timeout': 3600,
|
||||
}
|
||||
self.senlin_mock.create_cluster.assert_called_once_with(
|
||||
**expect_kwargs)
|
||||
self.senlin_mock.get_cluster.assert_called_once_with(self.fake_cl.id)
|
||||
|
||||
def test_cluster_create_error(self):
|
||||
cfg.CONF.set_override('action_retry_limit', 0)
|
||||
cluster = self._init_cluster(self.t)
|
||||
self.senlin_mock.create_cluster.return_value = self.fake_cl
|
||||
self.senlin_mock.get_cluster.return_value = FakeCluster(
|
||||
status='ERROR')
|
||||
create_task = scheduler.TaskRunner(cluster.create)
|
||||
ex = self.assertRaises(exception.ResourceFailure, create_task)
|
||||
expected = ('ResourceInError: resources.senlin-cluster: '
|
||||
'Went to status ERROR due to "Unknown"')
|
||||
self.assertEqual(expected, six.text_type(ex))
|
||||
|
||||
def test_cluster_delete_success(self):
|
||||
cluster = self._create_cluster(self.t)
|
||||
self.senlin_mock.get_cluster.side_effect = [
|
||||
exc.HTTPNotFound(),
|
||||
]
|
||||
scheduler.TaskRunner(cluster.delete)()
|
||||
self.senlin_mock.delete_cluster.assert_called_once_with(
|
||||
cluster.resource_id)
|
||||
|
||||
def test_cluster_delete_error(self):
|
||||
cluster = self._create_cluster(self.t)
|
||||
self.senlin_mock.get_cluster.side_effect = exception.Error('oops')
|
||||
delete_task = scheduler.TaskRunner(cluster.delete)
|
||||
ex = self.assertRaises(exception.ResourceFailure, delete_task)
|
||||
expected = 'Error: resources.senlin-cluster: oops'
|
||||
self.assertEqual(expected, six.text_type(ex))
|
||||
|
||||
def test_cluster_resolve_attribute(self):
|
||||
excepted_show = {
|
||||
'id': 'some_id',
|
||||
'status': 'ACTIVE',
|
||||
'status_reason': 'Unknown',
|
||||
'name': 'SenlinCluster',
|
||||
'metadata': {'foo': 'bar'},
|
||||
'timeout': 3600,
|
||||
'desired_capacity': 1,
|
||||
'max_size': -1,
|
||||
'min_size': 0,
|
||||
'nodes': ['node1']
|
||||
}
|
||||
cluster = self._create_cluster(self.t)
|
||||
self.assertEqual(self.fake_cl.desired_capacity,
|
||||
cluster._resolve_attribute('desired_capacity'))
|
||||
self.assertEqual(['node1'],
|
||||
cluster._resolve_attribute('nodes'))
|
||||
self.assertEqual(excepted_show,
|
||||
cluster._show_resource())
|
||||
|
||||
|
||||
class TestSenlinClusterValidation(common.HeatTestCase):
|
||||
def setUp(self):
|
||||
super(TestSenlinClusterValidation, self).setUp()
|
||||
self.t = template_format.parse(cluster_stack_template)
|
||||
|
||||
def test_invalid_min_max_size(self):
|
||||
self.t['resources']['senlin-cluster']['properties']['min_size'] = 2
|
||||
self.t['resources']['senlin-cluster']['properties']['max_size'] = 1
|
||||
stack = utils.parse_stack(self.t)
|
||||
ex = self.assertRaises(exception.StackValidationFailed,
|
||||
stack['senlin-cluster'].validate)
|
||||
self.assertEqual('min_size can not be greater than max_size',
|
||||
six.text_type(ex))
|
||||
|
||||
def test_invalid_desired_capacity(self):
|
||||
self.t['resources']['senlin-cluster']['properties']['min_size'] = 1
|
||||
self.t['resources']['senlin-cluster']['properties']['max_size'] = 2
|
||||
self.t['resources']['senlin-cluster']['properties'][
|
||||
'desired_capacity'] = 3
|
||||
stack = utils.parse_stack(self.t)
|
||||
ex = self.assertRaises(exception.StackValidationFailed,
|
||||
stack['senlin-cluster'].validate)
|
||||
self.assertEqual(
|
||||
'desired_capacity must be between min_size and max_size',
|
||||
six.text_type(ex)
|
||||
)
|
Loading…
Reference in New Issue
Block a user