Add OS::Senlin::Node resource

Add OS::Senlin::Node resource to heat, following is an example.
   node:
     type: OS::Senlin::Node
     properties:
       profile: p1

blueprint: senlin-resources
Change-Id: Iddf82cca7b311d7845bbc0f8e2d5a655cb60a8b8
This commit is contained in:
Ethan Lynn 2016-02-01 18:10:33 +08:00 committed by Ethan Lynn
parent 714627b39b
commit eaec5f9365
2 changed files with 375 additions and 0 deletions

View File

@ -0,0 +1,188 @@
#
# 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 Node(resource.Resource):
"""A resource that creates a Senlin Node.
Node is an object that belongs to at most one Cluster, it can be created
based on a profile.
"""
support_status = support.SupportStatus(version='6.0.0')
default_client_name = 'senlin'
PROPERTIES = (
NAME, METADATA, PROFILE,
) = (
'name', 'metadata', 'profile',
)
_NODE_STATUS = (
INIT, ACTIVE, CREATING,
) = (
'INIT', 'ACTIVE', 'CREATING',
)
_ACTION_STATUS = (
ACTION_SUCCEEDED, ACTION_FAILED,
) = (
'SUCCEEDED', 'FAILED',
)
ATTRIBUTES = (
ATTR_DETAILS, ATTR_CLUSTER,
) = (
'details', 'cluster_id'
)
properties_schema = {
NAME: properties.Schema(
properties.Schema.STRING,
_('Name of the senlin node. By default, physical resource name '
'is used.'),
update_allowed=True,
),
METADATA: properties.Schema(
properties.Schema.MAP,
_('Metadata key-values defined for node.'),
update_allowed=True,
),
PROFILE: properties.Schema(
properties.Schema.STRING,
_('Name or ID of senlin profile to create this node.'),
required=True,
update_allowed=True,
constraints=[
constraints.CustomConstraint('senlin.profile')
]
),
}
attributes_schema = {
ATTR_DETAILS: attributes.Schema(
_("The details of physical object."),
type=attributes.Schema.MAP
),
ATTR_CLUSTER: attributes.Schema(
_("The cluster ID this node belongs to."),
type=attributes.Schema.STRING
),
}
def handle_create(self):
params = {
'name': (self.properties[self.NAME] or
self.physical_resource_name()),
'metadata': self.properties[self.METADATA],
'profile_id': self.properties[self.PROFILE],
}
node = self.client().create_node(**params)
self.resource_id_set(node.id)
return node.id
def check_create_complete(self, resource_id):
node = self.client().get_node(resource_id)
if node.status == self.ACTIVE:
return True
elif node.status in [self.INIT, self.CREATING]:
return False
else:
raise exception.ResourceInError(
status_reason=node.status_reason,
resource_status=node.status,
)
def handle_delete(self):
if self.resource_id is not None:
with self.client_plugin().ignore_not_found:
self.client().delete_node(self.resource_id)
return self.resource_id
def check_delete_complete(self, res_id):
if not res_id:
return True
try:
self.client().get_node(self.resource_id)
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
return True
return False
def _show_resource(self):
node = self.client().get_node(self.resource_id)
return node.to_dict()
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
updaters = dict()
UPDATE_PROPS = [self.NAME, self.METADATA, self.PROFILE]
if any(p in prop_diff for p in UPDATE_PROPS):
params = dict((k, v) for k, v in prop_diff.items()
if k in UPDATE_PROPS)
if self.PROFILE in params:
params['profile_id'] = params.pop(self.PROFILE)
updaters['profile_update'] = {
'params': params,
'finished': False,
}
return updaters
def check_update_complete(self, updaters):
def check_action(updater, set_key):
action = self.client().get_action(updater['action'])
if action.status == self.ACTION_SUCCEEDED:
updater[set_key] = True
elif action.status == self.ACTION_FAILED:
raise exception.ResourceInError(
status_reason=action.status_reason,
resource_status=action.status,
)
if not updaters:
return True
profile_update = updaters.get('profile_update')
if profile_update and not profile_update['finished']:
if 'action' not in profile_update:
resp = self.client().update_node(
self.resource_id, **profile_update['params'])
profile_update['action'] = resp.location.split('/')[-1]
return False
else:
check_action(profile_update, 'finished')
if not profile_update['finished']:
return False
return True
def _resolve_attribute(self, name):
node = self.client().get_node(self.resource_id,
args={'show_details': True})
return getattr(node, name, None)
def resource_mapping():
return {
'OS::Senlin::Node': Node
}

View File

@ -0,0 +1,187 @@
#
# 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 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 node as sn
from heat.engine import scheduler
from heat.engine import template
from heat.tests import common
from heat.tests import utils
from senlinclient.common import exc
node_stack_template = """
heat_template_version: 2016-04-08
description: Senlin Node Template
resources:
senlin-node:
type: OS::Senlin::Node
properties:
name: SenlinNode
profile: fake_profile
metadata:
foo: bar
"""
class FakeNode(object):
def __init__(self, id='some_id', status='ACTIVE'):
self.status = status
self.status_reason = 'Unknown'
self.id = id
self.name = "SenlinNode"
self.metadata = {'foo': 'bar'}
self.profile_id = "fake_profile"
self.cluster_id = "fake_cluster"
self.details = {'id': 'physical_object_id'}
def to_dict(self):
return {
'id': self.id,
'status': self.status,
'status_reason': self.status_reason,
'name': self.name,
'metadata': self.metadata,
'profile_id': self.profile_id,
'cluster_id': self.cluster_id,
}
class SenlinNodeTest(common.HeatTestCase):
def setUp(self):
super(SenlinNodeTest, self).setUp()
self.senlin_mock = mock.MagicMock()
self.patchobject(sn.Node, 'client', return_value=self.senlin_mock)
self.patchobject(senlin.ProfileConstraint, 'validate',
return_value=True)
self.patchobject(senlin.ClusterConstraint, 'validate',
return_value=True)
self.fake_node = FakeNode()
self.t = template_format.parse(node_stack_template)
self.stack = utils.parse_stack(self.t)
self.node = self.stack['senlin-node']
def _create_node(self):
self.senlin_mock.create_node.return_value = self.fake_node
self.senlin_mock.get_node.return_value = self.fake_node
self.senlin_mock.get_action.return_value = mock.Mock(
status='SUCCEEDED')
scheduler.TaskRunner(self.node.create)()
self.assertEqual((self.node.CREATE, self.node.COMPLETE),
self.node.state)
self.assertEqual(self.fake_node.id, self.node.resource_id)
return self.node
def test_node_create_success(self):
self._create_node()
expect_kwargs = {
'name': 'SenlinNode',
'profile_id': 'fake_profile',
'metadata': {'foo': 'bar'},
}
self.senlin_mock.create_node.assert_called_once_with(
**expect_kwargs)
self.senlin_mock.get_node.assert_called_once_with(self.fake_node.id)
def test_node_create_error(self):
cfg.CONF.set_override('action_retry_limit', 0)
self.senlin_mock.create_node.return_value = self.fake_node
self.senlin_mock.get_node.return_value = FakeNode(
status='ERROR')
create_task = scheduler.TaskRunner(self.node.create)
ex = self.assertRaises(exception.ResourceFailure, create_task)
expected = ('ResourceInError: resources.senlin-node: '
'Went to status ERROR due to "Unknown"')
self.assertEqual(expected, six.text_type(ex))
def test_node_delete_success(self):
node = self._create_node()
self.senlin_mock.get_node.side_effect = [
exc.sdkexc.ResourceNotFound('SenlinNode'),
]
scheduler.TaskRunner(node.delete)()
self.senlin_mock.delete_node.assert_called_once_with(
node.resource_id)
def test_cluster_delete_error(self):
node = self._create_node()
self.senlin_mock.get_node.side_effect = exception.Error('oops')
delete_task = scheduler.TaskRunner(node.delete)
ex = self.assertRaises(exception.ResourceFailure, delete_task)
expected = 'Error: resources.senlin-node: oops'
self.assertEqual(expected, six.text_type(ex))
def test_node_update_profile(self):
node = self._create_node()
new_t = copy.deepcopy(self.t)
props = new_t['resources']['senlin-node']['properties']
props['profile'] = 'new_profile'
props['name'] = 'new_name'
rsrc_defns = template.Template(new_t).resource_definitions(self.stack)
new_node = rsrc_defns['senlin-node']
self.senlin_mock.update_node.return_value = mock.Mock(
location='/actions/fake-action')
scheduler.TaskRunner(node.update, new_node)()
self.assertEqual((node.UPDATE, node.COMPLETE), node.state)
node_update_kwargs = {
'profile_id': 'new_profile',
'name': 'new_name'
}
self.senlin_mock.update_node.assert_called_once_with(
node.resource_id, **node_update_kwargs)
self.senlin_mock.get_action.assert_called_once_with(
'fake-action')
def test_node_update_failed(self):
node = self._create_node()
new_t = copy.deepcopy(self.t)
props = new_t['resources']['senlin-node']['properties']
props['name'] = 'new_name'
rsrc_defns = template.Template(new_t).resource_definitions(self.stack)
new_node = rsrc_defns['senlin-node']
self.senlin_mock.update_node.return_value = mock.Mock(
location='/actions/fake-action')
self.senlin_mock.get_action.return_value = mock.Mock(
status='FAILED', status_reason='oops')
update_task = scheduler.TaskRunner(node.update, new_node)
ex = self.assertRaises(exception.ResourceFailure, update_task)
expected = ('ResourceInError: resources.senlin-node: Went to '
'status FAILED due to "oops"')
self.assertEqual(expected, six.text_type(ex))
self.assertEqual((node.UPDATE, node.FAILED), node.state)
self.senlin_mock.get_action.assert_called_once_with(
'fake-action')
def test_cluster_resolve_attribute(self):
excepted_show = {
'id': 'some_id',
'status': 'ACTIVE',
'status_reason': 'Unknown',
'name': 'SenlinNode',
'metadata': {'foo': 'bar'},
'profile_id': 'fake_profile',
'cluster_id': 'fake_cluster'
}
node = self._create_node()
self.assertEqual(excepted_show,
node._show_resource())
self.assertEqual(self.fake_node.details,
node._resolve_attribute('details'))