Add OS::Senlin::Policy resource

Add OS::Senlin::Policy resource to heat, following is an example
to use it.
  senlin-policy:
    type: OS::Senlin::Policy
    properties:
      name: SenlinPolicy
      type: senlin.policy.deletion-1.0
      properties:
        criteria: OLDEST_FIRST
      bindings:
        - cluster: c1

blueprint senlin-resources
Change-Id: If66fdfad9bb2eda163f1c030c548422ec45dbdd8
This commit is contained in:
Ethan Lynn 2016-02-02 12:18:09 +08:00 committed by Ethan Lynn
parent 86a7dc1894
commit ac82ec1201
4 changed files with 406 additions and 11 deletions

View File

@ -46,6 +46,10 @@ class SenlinClientPlugin(client_plugin.ClientPlugin):
def is_not_found(self, ex):
return isinstance(ex, exc.sdkexc.ResourceNotFound)
def is_bad_request(self, ex):
return (isinstance(ex, exc.sdkexc.HttpException) and
ex.http_status == 400)
class ProfileConstraint(constraints.BaseCustomConstraint):
# If name is not unique, will raise exc.sdkexc.HttpException

View File

@ -0,0 +1,207 @@
#
# 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
from heat.common import exception
from heat.common.i18n import _
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine import support
class Policy(resource.Resource):
"""A resource that creates a Senlin Policy.
A policy is a set of rules that can be checked and/or enforced when
an action is performed on a Cluster.
"""
support_status = support.SupportStatus(version='6.0.0')
default_client_name = 'senlin'
PROPERTIES = (
NAME, TYPE, POLICY_PROPS, BINDINGS,
) = (
'name', 'type', 'properties', 'bindings'
)
_BINDINGS = (
BD_CLUSTER, BD_ENABLED,
) = (
'cluster', 'enabled'
)
_ACTION_STATUS = (
ACTION_SUCCEEDED, ACTION_FAILED,
) = (
'SUCCEEDED', 'FAILED',
)
properties_schema = {
NAME: properties.Schema(
properties.Schema.STRING,
_('Name of the senlin policy. By default, physical resource name '
'is used.'),
update_allowed=True,
),
TYPE: properties.Schema(
properties.Schema.STRING,
_('The type of senlin policy.'),
required=True,
constraints=[
constraints.CustomConstraint('senlin.policy_type')
]
),
POLICY_PROPS: properties.Schema(
properties.Schema.MAP,
_('Properties of this policy.'),
),
BINDINGS: properties.Schema(
properties.Schema.LIST,
_('A list of clusters to which this policy is attached.'),
update_allowed=True,
schema=properties.Schema(
properties.Schema.MAP,
schema={
BD_CLUSTER: properties.Schema(
properties.Schema.STRING,
_("The name or ID of target cluster."),
required=True,
constraints=[
constraints.CustomConstraint('senlin.cluster')
]
),
BD_ENABLED: properties.Schema(
properties.Schema.BOOLEAN,
_("Whether enable this policy on that cluster."),
default=True,
),
}
)
)
}
def remove_bindings(self, bindings):
for bd in bindings:
try:
bd['action'] = self.client().cluster_detach_policy(
bd[self.BD_CLUSTER], self.resource_id)['action']
bd['finished'] = False
except Exception as ex:
# policy didn't attach to cluster, skip.
if (self.client_plugin().is_bad_request(ex) or
self.client_plugin().is_not_found(ex)):
bd['finished'] = True
else:
raise ex
def add_bindings(self, bindings):
for bd in bindings:
bd['action'] = self.client().cluster_attach_policy(
bd[self.BD_CLUSTER], self.resource_id,
enabled=bd[self.BD_ENABLED])['action']
bd['finished'] = False
def check_action_done(self, bindings):
ret = True
if not bindings:
return ret
for bd in bindings:
if bd.get('finished', False):
continue
action = self.client().get_action(bd['action'])
if action.status == self.ACTION_SUCCEEDED:
bd['finished'] = True
elif action.status == self.ACTION_FAILED:
err_msg = _('Failed to execute %(action)s for '
'%(cluster)s: %(reason)s') % {
'action': action.action,
'cluster': bd[self.BD_CLUSTER],
'reason': action.status_reason}
raise exception.ResourceInError(
status_reason=err_msg,
resource_status=self.FAILED)
else:
ret = False
return ret
def handle_create(self):
params = {
'name': (self.properties[self.NAME] or
self.physical_resource_name()),
'spec': self.client_plugin().generate_spec(
self.properties[self.TYPE],
self.properties[self.POLICY_PROPS]
)
}
policy = self.client().create_policy(**params)
self.resource_id_set(policy.id)
bindings = copy.deepcopy(self.properties[self.BINDINGS])
if bindings:
self.add_bindings(bindings)
return bindings
def check_create_complete(self, bindings):
return self.check_action_done(bindings)
def handle_delete(self):
return copy.deepcopy(self.properties[self.BINDINGS])
def check_delete_complete(self, bindings):
if not self.resource_id:
return True
self.remove_bindings(bindings)
if self.check_action_done(bindings):
with self.client_plugin().ignore_not_found:
self.client().delete_policy(self.resource_id)
return True
return False
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if self.NAME in prop_diff:
param = {'name': prop_diff[self.NAME]}
self.client().update_policy(self.resource_id, **param)
actions = dict()
if self.BINDINGS in prop_diff:
old = self.properties[self.BINDINGS] or []
new = prop_diff[self.BINDINGS] or []
actions['remove'] = [bd for bd in old if bd not in new]
actions['add'] = [bd for bd in new if bd not in old]
self.remove_bindings(actions['remove'])
return actions
def check_update_complete(self, actions):
ret = True
remove_done = self.check_action_done(actions.get('remove', []))
# wait until detach finished, then start attach
if remove_done and 'add' in actions:
if not actions.get('add_started', False):
self.add_bindings(actions['add'])
actions['add_started'] = True
ret = self.check_action_done(actions['add'])
return ret
def _show_resource(self):
policy = self.client().get_policy(self.resource_id)
return policy.to_dict()
def resource_mapping():
return {
'OS::Senlin::Policy': Policy
}

View File

@ -27,19 +27,14 @@ class SenlinClientPluginTests(common.HeatTestCase):
client = plugin.client()
self.assertIsNotNone(client.clusters)
def test_generate_spec(self):
def test_is_bad_request(self):
context = utils.dummy_context()
plugin = context.clients.client_plugin('senlin')
props = {'foo': 'bar'}
expect_spec = {
'type': 'os.heat.stack',
'version': '1.0',
'properties': {
'foo': 'bar'
}
}
self.assertEqual(expect_spec, plugin.generate_spec(
'os.heat.stack-1.0', props))
self.assertTrue(plugin.is_bad_request(
exc.sdkexc.HttpException(http_status=400)))
self.assertFalse(plugin.is_bad_request(Exception))
self.assertFalse(plugin.is_bad_request(
exc.sdkexc.HttpException(http_status=404)))
class ProfileConstraintTest(common.HeatTestCase):

View File

@ -0,0 +1,189 @@
#
# 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
from senlinclient.common import exc
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 policy
from heat.engine import scheduler
from heat.engine import template
from heat.tests import common
from heat.tests import utils
policy_stack_template = """
heat_template_version: 2016-04-08
description: Senlin Policy Template
resources:
senlin-policy:
type: OS::Senlin::Policy
properties:
name: SenlinPolicy
type: senlin.policy.deletion-1.0
properties:
criteria: OLDEST_FIRST
bindings:
- cluster: c1
"""
policy_spec = {
'type': 'senlin.policy.deletion',
'version': '1.0',
'properties': {
'criteria': 'OLDEST_FIRST'
}
}
class FakePolicy(object):
def __init__(self, id='some_id', spec=None):
self.id = id
self.name = "SenlinPolicy"
def to_dict(self):
return {
'id': self.id,
'name': self.name,
}
class SenlinPolicyTest(common.HeatTestCase):
def setUp(self):
super(SenlinPolicyTest, self).setUp()
self.patchobject(senlin.ClusterConstraint, 'validate',
return_value=True)
self.patchobject(senlin.PolicyTypeConstraint, 'validate',
return_value=True)
self.senlin_mock = mock.MagicMock()
self.patchobject(policy.Policy, 'client',
return_value=self.senlin_mock)
self.fake_p = FakePolicy()
self.t = template_format.parse(policy_stack_template)
def _init_policy(self, template):
self.stack = utils.parse_stack(template)
policy = self.stack['senlin-policy']
return policy
def _create_policy(self, template):
policy = self._init_policy(template)
self.senlin_mock.create_policy.return_value = self.fake_p
self.senlin_mock.cluster_attach_policy.return_value = {
'action': 'fake_action'}
self.senlin_mock.get_action.return_value = mock.Mock(
status='SUCCEEDED')
scheduler.TaskRunner(policy.create)()
self.assertEqual((policy.CREATE, policy.COMPLETE),
policy.state)
self.assertEqual(self.fake_p.id, policy.resource_id)
self.senlin_mock.cluster_attach_policy.assert_called_once_with(
'c1', policy.resource_id, enabled=True)
self.senlin_mock.get_action.assert_called_once_with('fake_action')
return policy
def test_policy_create(self):
self._create_policy(self.t)
expect_kwargs = {
'name': 'SenlinPolicy',
'spec': policy_spec
}
self.senlin_mock.create_policy.assert_called_once_with(
**expect_kwargs)
def test_policy_create_fail(self):
cfg.CONF.set_override('action_retry_limit', 0)
policy = self._init_policy(self.t)
self.senlin_mock.create_policy.return_value = self.fake_p
self.senlin_mock.cluster_attach_policy.return_value = {
'action': 'fake_action'}
self.senlin_mock.get_action.return_value = mock.Mock(
status='FAILED', status_reason='oops',
action='CLUSTER_ATTACH_POLICY')
create_task = scheduler.TaskRunner(policy.create)
self.assertRaises(exception.ResourceFailure, create_task)
self.assertEqual((policy.CREATE, policy.FAILED),
policy.state)
err_msg = ('ResourceInError: resources.senlin-policy: Went to status '
'FAILED due to "Failed to execute CLUSTER_ATTACH_POLICY '
'for c1: oops"')
self.assertEqual(err_msg, policy.status_reason)
def test_policy_delete_not_found(self):
self.senlin_mock.cluster_detach_policy.return_value = {
'action': 'fake_action'}
policy = self._create_policy(self.t)
self.senlin_mock.get_policy.side_effect = [
exc.sdkexc.ResourceNotFound('SenlinPolicy'),
]
scheduler.TaskRunner(policy.delete)()
self.senlin_mock.cluster_detach_policy.assert_called_once_with(
'c1', policy.resource_id)
self.senlin_mock.delete_policy.assert_called_once_with(
policy.resource_id)
def test_policy_delete_not_attached(self):
policy = self._create_policy(self.t)
self.senlin_mock.get_policy.side_effect = [
exc.sdkexc.ResourceNotFound('SenlinPolicy'),
]
self.senlin_mock.cluster_detach_policy.side_effect = [
exc.sdkexc.HttpException(http_status=400),
]
scheduler.TaskRunner(policy.delete)()
self.senlin_mock.cluster_detach_policy.assert_called_once_with(
'c1', policy.resource_id)
self.senlin_mock.delete_policy.assert_called_once_with(
policy.resource_id)
def test_policy_update(self):
policy = self._create_policy(self.t)
new_t = copy.deepcopy(self.t)
props = new_t['resources']['senlin-policy']['properties']
props['bindings'] = [{'cluster': 'c2'}]
props['name'] = 'new_name'
rsrc_defns = template.Template(new_t).resource_definitions(self.stack)
new_cluster = rsrc_defns['senlin-policy']
self.senlin_mock.cluster_attach_policy.return_value = {
'action': 'fake_action1'}
self.senlin_mock.cluster_detach_policy.return_value = {
'action': 'fake_action2'}
scheduler.TaskRunner(policy.update, new_cluster)()
self.assertEqual((policy.UPDATE, policy.COMPLETE), policy.state)
self.senlin_mock.update_policy.assert_called_once_with(
policy.resource_id, name='new_name')
self.senlin_mock.cluster_detach_policy.assert_called_once_with(
'c1', policy.resource_id)
self.senlin_mock.cluster_attach_policy.assert_called_with(
'c2', policy.resource_id, enabled=True)
def test_policy_resolve_attribute(self):
excepted_show = {
'id': 'some_id',
'name': 'SenlinPolicy',
}
policy = self._create_policy(self.t)
self.senlin_mock.get_policy.return_value = FakePolicy()
self.assertEqual(excepted_show, policy._show_resource())
def test_resource_mapping(self):
mapping = policy.resource_mapping()
self.assertEqual(1, len(mapping))
self.assertEqual(policy.Policy,
mapping['OS::Senlin::Policy'])