Merge "Add OS::Senlin::Policy resource"
This commit is contained in:
commit
e19641cb16
@ -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
|
||||
|
207
heat/engine/resources/openstack/senlin/policy.py
Normal file
207
heat/engine/resources/openstack/senlin/policy.py
Normal 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
|
||||
}
|
@ -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):
|
||||
|
189
heat/tests/openstack/senlin/test_policy.py
Normal file
189
heat/tests/openstack/senlin/test_policy.py
Normal 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'])
|
Loading…
x
Reference in New Issue
Block a user