diff --git a/contrib/heat_mistral/heat_mistral/resources/cron_trigger.py b/contrib/heat_mistral/heat_mistral/resources/cron_trigger.py new file mode 100644 index 0000000000..7890f2f952 --- /dev/null +++ b/contrib/heat_mistral/heat_mistral/resources/cron_trigger.py @@ -0,0 +1,119 @@ +# +# 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.i18n import _ +from heat.engine import attributes +from heat.engine import properties +from heat.engine import resource +from heat.engine import support + + +class CronTrigger(resource.Resource): + support_status = support.SupportStatus(version='2015.2') + + PROPERTIES = ( + NAME, PATTERN, WORKFLOW, FIRST_TIME, COUNT + ) = ( + 'name', 'pattern', 'workflow', 'first_time', 'count' + ) + + _WORKFLOW_KEYS = ( + WORKFLOW_NAME, WORKFLOW_INPUT + ) = ( + 'name', 'input' + ) + + ATTRIBUTES = ( + NEXT_EXECUTION_TIME, + ) = ( + 'next_execution_time', + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('Name of the cron trigger.') + ), + PATTERN: properties.Schema( + properties.Schema.STRING, + _('Cron expression.') + ), + WORKFLOW: properties.Schema( + properties.Schema.MAP, + _('Workflow to execute.'), + required=True, + schema={ + WORKFLOW_NAME: properties.Schema( + properties.Schema.STRING, + _('Name of the workflow.') + ), + WORKFLOW_INPUT: properties.Schema( + properties.Schema.MAP, + _('Input values for the workflow.') + ) + } + ), + FIRST_TIME: properties.Schema( + properties.Schema.STRING, + _('Time of the first execution in format "YYYY-MM-DD HH:MM".') + ), + COUNT: properties.Schema( + properties.Schema.INTEGER, + _('Remaining executions.') + ) + } + + attributes_schema = { + NEXT_EXECUTION_TIME: attributes.Schema( + _('Time of the next execution in format "YYYY-MM-DD HH:MM".') + ), + } + + default_client_name = 'mistral' + + def _cron_trigger_name(self): + return self.properties.get(self.NAME) or self.physical_resource_name() + + def handle_create(self): + workflow = self.properties.get(self.WORKFLOW) + args = { + 'name': self._cron_trigger_name(), + 'pattern': self.properties.get(self.PATTERN), + 'workflow_name': workflow.get(self.WORKFLOW_NAME), + 'workflow_input': workflow.get(self.WORKFLOW_INPUT), + 'first_time': self.properties.get(self.FIRST_TIME), + 'count': self.properties.get(self.COUNT) + } + + cron_trigger = self.client().cron_triggers.create(**args) + self.resource_id_set(cron_trigger.name) + + def handle_delete(self): + if not self.resource_id: + return + + try: + self.client().cron_triggers.delete(self.resource_id) + except Exception as ex: + self.client_plugin().ignore_not_found(ex) + + def _resolve_attribute(self, name): + if name == self.NEXT_EXECUTION_TIME: + trigger = self.client().cron_triggers.get(self.resource_id) + return trigger.next_execution_time + + +def resource_mapping(): + return { + 'OS::Mistral::CronTrigger': CronTrigger, + } diff --git a/contrib/heat_mistral/heat_mistral/tests/test_cron_trigger.py b/contrib/heat_mistral/heat_mistral/tests/test_cron_trigger.py new file mode 100644 index 0000000000..62c94dd308 --- /dev/null +++ b/contrib/heat_mistral/heat_mistral/tests/test_cron_trigger.py @@ -0,0 +1,107 @@ +# +# 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 heat.common import template_format +from heat.engine import scheduler +from heat.tests import common +from heat.tests import utils + +from ..resources import cron_trigger # noqa + +stack_template = ''' +heat_template_version: 2013-05-23 + +resources: + cron_trigger: + type: OS::Mistral::CronTrigger + properties: + name: my_cron_trigger + pattern: "* * 0 * *" + workflow: {'name': 'get_first_glance_image', 'input': {} } + count: 3 + first_time: "2015-04-08 06:20" +''' + + +class FakeCronTrigger(object): + + def __init__(self, name): + self.name = name + self.next_execution_time = '2015-03-01 00:00:00' + + +class CronTriggerTest(common.HeatTestCase): + + def setUp(self): + super(CronTriggerTest, self).setUp() + utils.setup_dummy_db() + self.ctx = utils.dummy_context() + + t = template_format.parse(stack_template) + self.stack = utils.parse_stack(t) + resource_defns = self.stack.t.resource_definitions(self.stack) + self.rsrc_defn = resource_defns['cron_trigger'] + + self.client = mock.Mock() + self.patchobject(cron_trigger.CronTrigger, 'client', + return_value=self.client) + + def _create_resource(self, name, snippet, stack): + ct = cron_trigger.CronTrigger(name, snippet, stack) + self.client.cron_triggers.create.return_value = FakeCronTrigger( + 'my_cron_trigger') + self.client.cron_triggers.get.return_value = FakeCronTrigger( + 'my_cron_trigger') + scheduler.TaskRunner(ct.create)() + args = self.client.cron_triggers.create.call_args[1] + self.assertEqual('* * 0 * *', args['pattern']) + self.assertEqual('get_first_glance_image', args['workflow_name']) + self.assertEqual({}, args['workflow_input']) + self.assertEqual('2015-04-08 06:20', args['first_time']) + self.assertEqual(3, args['count']) + self.assertEqual('my_cron_trigger', ct.resource_id) + return ct + + def test_create(self): + ct = self._create_resource('trigger', self.rsrc_defn, self.stack) + expected_state = (ct.CREATE, ct.COMPLETE) + self.assertEqual(expected_state, ct.state) + + def test_resource_mapping(self): + mapping = cron_trigger.resource_mapping() + self.assertEqual(1, len(mapping)) + self.assertEqual(cron_trigger.CronTrigger, + mapping['OS::Mistral::CronTrigger']) + + def test_attributes(self): + ct = self._create_resource('trigger', self.rsrc_defn, self.stack) + self.assertEqual('2015-03-01 00:00:00', + ct.FnGetAtt('next_execution_time')) + + def test_delete(self): + ct = self._create_resource('trigger', self.rsrc_defn, self.stack) + scheduler.TaskRunner(ct.delete)() + self.assertEqual((ct.DELETE, ct.COMPLETE), ct.state) + self.client.cron_triggers.delete.assert_called_once_with( + ct.resource_id) + + def test_delete_not_found(self): + ct = self._create_resource('trigger', self.rsrc_defn, self.stack) + self.client.cron_triggers.delete.side_effect = ( + self.client.mistral_base.APIException(error_code=404)) + scheduler.TaskRunner(ct.delete)() + self.assertEqual((ct.DELETE, ct.COMPLETE), ct.state) + self.client.cron_triggers.delete.assert_called_once_with( + ct.resource_id)