Skip to content

Commit 725b404

Browse files
committed
Implements custom resource type managed by Mistral workflows
Adds a new resource named OS::Mistral::ExternalResource which allow users to use custom resource types implementing their actions as Mistral workflows. Implements: blueprint mistral-new-resource-type-workflow-execution Change-Id: If02799e7457ca017cc119317dfb2db7198a3559f
1 parent f206164 commit 725b404

3 files changed

Lines changed: 422 additions & 0 deletions

File tree

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
#
2+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
3+
# not use this file except in compliance with the License. You may obtain
4+
# a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
# License for the specific language governing permissions and limitations
12+
# under the License.
13+
14+
from oslo_log import log as logging
15+
from oslo_serialization import jsonutils
16+
import six
17+
18+
from heat.common import exception
19+
from heat.common.i18n import _
20+
from heat.engine import attributes
21+
from heat.engine import constraints
22+
from heat.engine import properties
23+
from heat.engine import resource
24+
from heat.engine import support
25+
26+
LOG = logging.getLogger(__name__)
27+
28+
29+
class MistralExternalResource(resource.Resource):
30+
"""A plugin for managing user-defined resources via Mistral workflows.
31+
32+
This resource allows users to manage resources that are not known to Heat.
33+
The user may specify a Mistral workflow to handle each resource action,
34+
such as CREATE, UPDATE, or DELETE.
35+
36+
The workflows may return an output named 'resource_id', which will be
37+
treated as the physical ID of the resource by Heat.
38+
39+
Once the resource is created, subsequent workflow runs will receive the
40+
output of the last workflow execution in the 'heat_extresource_data' key
41+
in the workflow environment (accessible as ``env().heat_extresource_data``
42+
in the workflow).
43+
44+
The template author may specify a subset of inputs as causing replacement
45+
of the resource when they change, as an alternative to running the
46+
UPDATE workflow.
47+
"""
48+
49+
support_status = support.SupportStatus(version='9.0.0')
50+
51+
default_client_name = 'mistral'
52+
53+
entity = 'executions'
54+
55+
_ACTION_PROPERTIES = (
56+
WORKFLOW, PARAMS
57+
) = (
58+
'workflow', 'params'
59+
)
60+
61+
PROPERTIES = (
62+
EX_ACTIONS,
63+
INPUT,
64+
DESCRIPTION,
65+
REPLACE_ON_CHANGE,
66+
ALWAYS_UPDATE
67+
) = (
68+
'actions',
69+
'input',
70+
'description',
71+
'replace_on_change_inputs',
72+
'always_update'
73+
)
74+
75+
ATTRIBUTES = (
76+
OUTPUT,
77+
) = (
78+
'output',
79+
)
80+
81+
_action_properties_schema = properties.Schema(
82+
properties.Schema.MAP,
83+
_('Dictionary which defines the workflow to run and its params.'),
84+
schema={
85+
WORKFLOW: properties.Schema(
86+
properties.Schema.STRING,
87+
_('Workflow to execute.'),
88+
required=True,
89+
constraints=[
90+
constraints.CustomConstraint('mistral.workflow')
91+
],
92+
),
93+
PARAMS: properties.Schema(
94+
properties.Schema.MAP,
95+
_('Workflow additional parameters. If workflow is reverse '
96+
'typed, params requires "task_name", which defines '
97+
'initial task.'),
98+
default={}
99+
),
100+
}
101+
)
102+
103+
properties_schema = {
104+
EX_ACTIONS: properties.Schema(
105+
properties.Schema.MAP,
106+
_('Resource action which triggers a workflow execution.'),
107+
schema={
108+
resource.Resource.CREATE: _action_properties_schema,
109+
resource.Resource.UPDATE: _action_properties_schema,
110+
resource.Resource.SUSPEND: _action_properties_schema,
111+
resource.Resource.RESUME: _action_properties_schema,
112+
resource.Resource.DELETE: _action_properties_schema,
113+
},
114+
required=True
115+
),
116+
INPUT: properties.Schema(
117+
properties.Schema.MAP,
118+
_('Dictionary which contains input for the workflows.'),
119+
update_allowed=True,
120+
default={}
121+
),
122+
DESCRIPTION: properties.Schema(
123+
properties.Schema.STRING,
124+
_('Workflow execution description.'),
125+
default='Heat managed'
126+
),
127+
REPLACE_ON_CHANGE: properties.Schema(
128+
properties.Schema.LIST,
129+
_('A list of inputs that should cause the resource to be replaced '
130+
'when their values change.'),
131+
default=[]
132+
),
133+
ALWAYS_UPDATE: properties.Schema(
134+
properties.Schema.BOOLEAN,
135+
_('Triggers UPDATE action execution even if input is '
136+
'unchanged.'),
137+
default=False
138+
),
139+
}
140+
141+
attributes_schema = {
142+
OUTPUT: attributes.Schema(
143+
_('Output from the execution.'),
144+
type=attributes.Schema.MAP
145+
),
146+
}
147+
148+
def _check_execution(self, action, execution_id):
149+
"""Check execution status.
150+
151+
Returns False if in IDLE, RUNNING or PAUSED
152+
returns True if in SUCCESS
153+
raises ResourceFailure if in ERROR, CANCELLED
154+
raises ResourceUnknownState otherwise.
155+
"""
156+
execution = self.client().executions.get(execution_id)
157+
LOG.debug('Mistral execution %(id)s is in state '
158+
'%(state)s' % {'id': execution_id,
159+
'state': execution.state})
160+
161+
if execution.state in ('IDLE', 'RUNNING', 'PAUSED'):
162+
return False, {}
163+
164+
if execution.state in ('SUCCESS',):
165+
return True, jsonutils.loads(execution.output)
166+
167+
if execution.state in ('ERROR', 'CANCELLED'):
168+
raise exception.ResourceFailure(
169+
exception_or_error=execution.state,
170+
resource=self,
171+
action=action)
172+
173+
raise exception.ResourceUnknownStatus(
174+
resource_status=execution.state,
175+
result=_('Mistral execution is in unknown state.'))
176+
177+
def _handle_action(self, action, inputs=None):
178+
action_data = self.properties[self.EX_ACTIONS].get(action)
179+
if action_data:
180+
# bring forward output from previous executions into env
181+
if self.resource_id:
182+
old_outputs = jsonutils.loads(self.data().get('outputs', '{}'))
183+
action_env = action_data[self.PARAMS].get('env', {})
184+
action_env['heat_extresource_data'] = old_outputs
185+
action_data[self.PARAMS]['env'] = action_env
186+
# inputs is not None when inputs changed on stack UPDATE
187+
if not inputs:
188+
inputs = self.properties[self.INPUT]
189+
execution = self.client().executions.create(
190+
action_data[self.WORKFLOW],
191+
jsonutils.dumps(inputs),
192+
self.properties[self.DESCRIPTION],
193+
**action_data[self.PARAMS])
194+
LOG.debug('Mistral execution %(id)s params set to '
195+
'%(params)s' % {'id': execution.id,
196+
'params': action_data[self.PARAMS]})
197+
return execution.id
198+
199+
def _check_action(self, action, execution_id):
200+
success = True
201+
# execution_id is None when no data is available for a given action
202+
if execution_id:
203+
rsrc_id = execution_id
204+
success, output = self._check_execution(action, execution_id)
205+
# merge output with outputs of previous executions
206+
outputs = jsonutils.loads(self.data().get('outputs', '{}'))
207+
outputs.update(output)
208+
self.data_set('outputs', jsonutils.dumps(outputs))
209+
# set resource id using output, if found
210+
if output.get('resource_id'):
211+
rsrc_id = output.get('resource_id')
212+
LOG.debug('ExternalResource id set to %(rid)s from Mistral '
213+
'execution %(eid)s output' % {'eid': execution_id,
214+
'rid': rsrc_id})
215+
self.resource_id_set(six.text_type(rsrc_id)[:255])
216+
return success
217+
218+
def _resolve_attribute(self, name):
219+
if self.resource_id and name == self.OUTPUT:
220+
return self.data().get('outputs')
221+
222+
def _needs_update(self, after, before, after_props, before_props,
223+
prev_resource, check_init_complete=True):
224+
# check if we need to force replace first
225+
old_inputs = before_props[self.INPUT]
226+
new_inputs = after_props[self.INPUT]
227+
for i in after_props[self.REPLACE_ON_CHANGE]:
228+
if old_inputs.get(i) != new_inputs.get(i):
229+
LOG.debug('Replacing ExternalResource %(id)s instead of '
230+
'updating due to change to input "%(i)s"' %
231+
{"id": self.resource_id,
232+
"i": i})
233+
raise resource.UpdateReplace(self)
234+
# honor always_update if found
235+
if self.properties[self.ALWAYS_UPDATE]:
236+
return True
237+
# call super in all other scenarios
238+
else:
239+
return super(MistralExternalResource,
240+
self)._needs_update(after,
241+
before,
242+
after_props,
243+
before_props,
244+
prev_resource,
245+
check_init_complete)
246+
247+
def handle_create(self):
248+
return self._handle_action(self.CREATE)
249+
250+
def check_create_complete(self, execution_id):
251+
return self._check_action(self.CREATE, execution_id)
252+
253+
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
254+
new_inputs = prop_diff.get(self.INPUT)
255+
return self._handle_action(self.UPDATE, new_inputs)
256+
257+
def check_update_complete(self, execution_id):
258+
return self._check_action(self.UPDATE, execution_id)
259+
260+
def handle_suspend(self):
261+
return self._handle_action(self.SUSPEND)
262+
263+
def check_suspend_complete(self, execution_id):
264+
return self._check_action(self.SUSPEND, execution_id)
265+
266+
def handle_resume(self):
267+
return self._handle_action(self.RESUME)
268+
269+
def check_resume_complete(self, execution_id):
270+
return self._check_action(self.RESUME, execution_id)
271+
272+
def handle_delete(self):
273+
return self._handle_action(self.DELETE)
274+
275+
def check_delete_complete(self, execution_id):
276+
return self._check_action(self.DELETE, execution_id)
277+
278+
279+
def resource_mapping():
280+
return {
281+
'OS::Mistral::ExternalResource': MistralExternalResource
282+
}

0 commit comments

Comments
 (0)