1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5
# not use this file except in compliance with the License. You may obtain
6
# a copy of the License at
8
# http://www.apache.org/licenses/LICENSE-2.0
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
# License for the specific language governing permissions and limitations
17
from datetime import datetime
19
from heat.engine import event
20
from heat.common import exception
21
from heat.openstack.common import excutils
22
from heat.db import api as db_api
23
from heat.common import identifier
24
from heat.common import short_id
25
from heat.engine import timestamp
26
# import class to avoid name collisions and ugly aliasing
27
from heat.engine.attributes import Attributes
28
from heat.engine.properties import Properties
30
from heat.openstack.common import log as logging
31
from heat.openstack.common.gettextutils import _
33
logger = logging.getLogger(__name__)
36
_resource_classes = {}
37
_template_class = None
41
'''Return an iterator over the list of valid resource types.'''
42
return iter(_resource_classes)
45
def get_class(resource_type, resource_name=None, environment=None):
46
'''Return the Resource class for a given resource type.'''
48
resource_type = environment.get_resource_type(resource_type,
51
if resource_type.endswith(('.yaml', '.template')):
54
cls = _resource_classes.get(resource_type)
56
msg = "Unknown resource Type : %s" % resource_type
57
raise exception.StackValidationFailed(message=msg)
62
def _register_class(resource_type, resource_class):
63
logger.info(_('Registering resource type %s') % resource_type)
64
if resource_type in _resource_classes:
65
logger.warning(_('Replacing existing resource type %s') %
68
_resource_classes[resource_type] = resource_class
71
def register_template_class(cls):
72
global _template_class
73
if _template_class is None:
77
class UpdateReplace(Exception):
79
Raised when resource update requires replacement
81
_message = _("The Resource %s requires replacement.")
83
def __init__(self, resource_name='Unknown',
84
message=_("The Resource %s requires replacement.")):
86
msg = message % resource_name
89
super(Exception, self).__init__(msg)
92
class Metadata(object):
94
A descriptor for accessing the metadata of a resource while ensuring the
95
most up-to-date data is always obtained from the database.
98
def __get__(self, resource, resource_class):
99
'''Return the metadata for the owning resource.'''
102
if resource.id is None:
103
return resource.parsed_template('Metadata')
104
rs = db_api.resource_get(resource.stack.context, resource.id)
105
rs.refresh(attrs=['rsrc_metadata'])
106
return rs.rsrc_metadata
108
def __set__(self, resource, metadata):
109
'''Update the metadata for the owning resource.'''
110
if resource.id is None:
111
raise exception.ResourceNotAvailable(resource_name=resource.name)
112
rs = db_api.resource_get(resource.stack.context, resource.id)
113
rs.update_and_save({'rsrc_metadata': metadata})
116
class Resource(object):
117
ACTIONS = (CREATE, DELETE, UPDATE, ROLLBACK, SUSPEND, RESUME
118
) = ('CREATE', 'DELETE', 'UPDATE', 'ROLLBACK',
121
STATUSES = (IN_PROGRESS, FAILED, COMPLETE
122
) = ('IN_PROGRESS', 'FAILED', 'COMPLETE')
124
# If True, this resource must be created before it can be referenced.
125
strict_dependency = True
127
created_time = timestamp.Timestamp(db_api.resource_get, 'created_at')
128
updated_time = timestamp.Timestamp(db_api.resource_get, 'updated_at')
130
metadata = Metadata()
132
# Resource implementation set this to the subset of template keys
133
# which are supported for handle_update, used by update_template_diff
134
update_allowed_keys = ()
136
# Resource implementation set this to the subset of resource properties
137
# supported for handle_update, used by update_template_diff_properties
138
update_allowed_properties = ()
140
# Resource implementations set this to the name: description dictionary
141
# that describes the appropriate resource attributes
142
attributes_schema = {}
144
def __new__(cls, name, json, stack):
145
'''Create a new Resource of the appropriate class for its type.'''
148
# Call is already for a subclass, so pass it through
149
return super(Resource, cls).__new__(cls)
151
# Select the correct subclass to instantiate
152
ResourceClass = get_class(json['Type'],
154
environment=stack.env)
155
return ResourceClass(name, json, stack)
157
def __init__(self, name, json_snippet, stack):
159
raise ValueError(_('Resource name may not contain "/"'))
162
self.context = stack.context
164
self.json_snippet = json_snippet
165
self.t = stack.resolve_static_data(json_snippet)
166
self.properties = Properties(self.properties_schema,
167
self.t.get('Properties', {}),
168
self.stack.resolve_runtime_data,
170
self.attributes = Attributes(self.name,
171
self.attributes_schema,
172
self._resolve_attribute)
174
resource = db_api.resource_get_by_name_and_stack(self.context,
177
self.resource_id = resource.nova_instance
178
self.action = resource.action
179
self.status = resource.status
180
self.status_reason = resource.status_reason
181
self.id = resource.id
182
self.data = resource.data
184
self.resource_id = None
187
self.status_reason = ''
191
def __eq__(self, other):
192
'''Allow == comparison of two resources.'''
193
# For the purposes of comparison, we declare two resource objects
194
# equal if their names and parsed_templates are the same
195
if isinstance(other, Resource):
196
return (self.name == other.name) and (
197
self.parsed_template() == other.parsed_template())
198
return NotImplemented
200
def __ne__(self, other):
201
'''Allow != comparison of two resources.'''
202
result = self.__eq__(other)
203
if result is NotImplemented:
208
return self.t['Type']
210
def identifier(self):
211
'''Return an identifier for this resource.'''
212
return identifier.ResourceIdentifier(resource_name=self.name,
213
**self.stack.identifier())
215
def parsed_template(self, section=None, default={}):
217
Return the parsed template data for the resource. May be limited to
218
only one section of the data, in which case a default value may also
224
template = self.t.get(section, default)
225
return self.stack.resolve_runtime_data(template)
227
def update_template_diff(self, after, before):
229
Returns the difference between the before and after json snippets. If
230
something has been removed in after which exists in before we set it to
231
None. If any keys have changed which are not in update_allowed_keys,
232
raises UpdateReplace if the differing keys are not in
235
update_allowed_set = set(self.update_allowed_keys)
237
# Create a set containing the keys in both current and update template
238
template_keys = set(before.keys())
239
template_keys.update(set(after.keys()))
241
# Create a set of keys which differ (or are missing/added)
242
changed_keys_set = set([k for k in template_keys
243
if before.get(k) != after.get(k)])
245
if not changed_keys_set.issubset(update_allowed_set):
246
badkeys = changed_keys_set - update_allowed_set
247
raise UpdateReplace(self.name)
249
return dict((k, after.get(k)) for k in changed_keys_set)
251
def update_template_diff_properties(self, after, before):
253
Returns the changed Properties between the before and after json
254
snippets. If a property has been removed in after which exists in
255
before we set it to None. If any properties have changed which are not
256
in update_allowed_properties, raises UpdateReplace if the modified
257
properties are not in the update_allowed_properties
259
update_allowed_set = set(self.update_allowed_properties)
261
# Create a set containing the keys in both current and update template
262
current_properties = before.get('Properties', {})
264
template_properties = set(current_properties.keys())
265
updated_properties = after.get('Properties', {})
266
template_properties.update(set(updated_properties.keys()))
268
# Create a set of keys which differ (or are missing/added)
269
changed_properties_set = set(k for k in template_properties
270
if current_properties.get(k) !=
271
updated_properties.get(k))
273
if not changed_properties_set.issubset(update_allowed_set):
274
raise UpdateReplace(self.name)
276
return dict((k, updated_properties.get(k))
277
for k in changed_properties_set)
280
return '%s "%s"' % (self.__class__.__name__, self.name)
282
def _add_dependencies(self, deps, head, fragment):
283
if isinstance(fragment, dict):
284
for key, value in fragment.items():
285
if key in ('DependsOn', 'Ref', 'Fn::GetAtt'):
286
if key == 'Fn::GetAtt':
290
target = self.stack.resources[value]
292
raise exception.InvalidTemplateReference(
295
if key == 'DependsOn' or target.strict_dependency:
296
deps += (self, target)
298
self._add_dependencies(deps, key, value)
299
elif isinstance(fragment, list):
300
for item in fragment:
301
self._add_dependencies(deps, head, item)
303
def add_dependencies(self, deps):
304
self._add_dependencies(deps, None, self.t)
307
def required_by(self):
309
Returns a list of names of resources which directly require this
310
resource as a dependency.
313
[r.name for r in self.stack.dependencies.required_by(self)])
316
return self.stack.clients.keystone()
318
def nova(self, service_type='compute'):
319
return self.stack.clients.nova(service_type)
322
return self.stack.clients.swift()
325
return self.stack.clients.quantum()
328
return self.stack.clients.cinder()
330
def _do_action(self, action, pre_func=None):
332
Perform a transition to a new state via a specified action
333
action should be e.g self.CREATE, self.UPDATE etc, we set
334
status based on this, the transistion is handled by calling the
335
corresponding handle_* and check_*_complete functions
336
Note pre_func is an optional function reference which will
337
be called before the handle_<action> function
339
If the resource does not declare a check_$action_complete function,
340
we declare COMPLETE status as soon as the handle_$action call has
341
finished, and if no handle_$action function is declared, then we do
342
nothing, useful e.g if the resource requires no action for a given
345
assert action in self.ACTIONS, 'Invalid action %s' % action
348
self.state_set(action, self.IN_PROGRESS)
350
action_l = action.lower()
351
handle = getattr(self, 'handle_%s' % action_l, None)
352
check = getattr(self, 'check_%s_complete' % action_l, None)
354
if callable(pre_func):
359
handle_data = handle()
362
while not check(handle_data):
364
except Exception as ex:
365
logger.exception('%s : %s' % (action, str(self)))
366
failure = exception.ResourceFailure(ex)
367
self.state_set(action, self.FAILED, str(failure))
370
with excutils.save_and_reraise_exception():
372
self.state_set(action, self.FAILED,
373
'%s aborted' % action)
375
logger.exception('Error marking resource as failed')
377
self.state_set(action, self.COMPLETE)
381
Create the resource. Subclasses should provide a handle_create() method
382
to customise creation.
384
assert None in (self.action, self.status), 'invalid state for create'
386
logger.info('creating %s' % str(self))
388
# Re-resolve the template, since if the resource Ref's
389
# the AWS::StackId pseudo parameter, it will change after
390
# the parser.Stack is stored (which is after the resources
391
# are __init__'d, but before they are create()'d)
392
self.t = self.stack.resolve_static_data(self.json_snippet)
393
self.properties = Properties(self.properties_schema,
394
self.t.get('Properties', {}),
395
self.stack.resolve_runtime_data,
397
return self._do_action(self.CREATE, self.properties.validate)
399
def update(self, after, before=None):
401
update the resource. Subclasses should provide a handle_update() method
402
to customise update, the base-class handle_update will fail by default.
405
before = self.parsed_template()
407
if (self.action, self.status) in ((self.CREATE, self.IN_PROGRESS),
408
(self.UPDATE, self.IN_PROGRESS)):
409
raise exception.ResourceFailure(Exception(
410
'Resource update already requested'))
412
logger.info('updating %s' % str(self))
415
self.state_set(self.UPDATE, self.IN_PROGRESS)
416
properties = Properties(self.properties_schema,
417
after.get('Properties', {}),
418
self.stack.resolve_runtime_data,
420
properties.validate()
421
tmpl_diff = self.update_template_diff(after, before)
422
prop_diff = self.update_template_diff_properties(after, before)
423
if callable(getattr(self, 'handle_update', None)):
424
result = self.handle_update(after, tmpl_diff, prop_diff)
425
except UpdateReplace:
426
logger.debug("Resource %s update requires replacement" % self.name)
428
except Exception as ex:
429
logger.exception('update %s : %s' % (str(self), str(ex)))
430
failure = exception.ResourceFailure(ex)
431
self.state_set(self.UPDATE, self.FAILED, str(failure))
434
self.t = self.stack.resolve_static_data(after)
435
self.state_set(self.UPDATE, self.COMPLETE)
439
Suspend the resource. Subclasses should provide a handle_suspend()
440
method to implement suspend
442
# Don't try to suspend the resource unless it's in a stable state
443
if (self.action == self.DELETE or self.status != self.COMPLETE):
444
exc = exception.Error('State %s invalid for suspend'
446
raise exception.ResourceFailure(exc)
448
logger.info('suspending %s' % str(self))
449
return self._do_action(self.SUSPEND)
453
Resume the resource. Subclasses should provide a handle_resume()
454
method to implement resume
456
# Can't resume a resource unless it's SUSPEND_COMPLETE
457
if self.state != (self.SUSPEND, self.COMPLETE):
458
exc = exception.Error('State %s invalid for resume'
460
raise exception.ResourceFailure(exc)
462
logger.info('resuming %s' % str(self))
463
return self._do_action(self.RESUME)
465
def physical_resource_name(self):
469
return '%s-%s-%s' % (self.stack.name,
471
short_id.get_id(self.id))
474
logger.info('Validating %s' % str(self))
476
self.validate_deletion_policy(self.t)
477
return self.properties.validate()
480
def validate_deletion_policy(cls, template):
481
deletion_policy = template.get('DeletionPolicy', 'Delete')
482
if deletion_policy not in ('Delete', 'Retain', 'Snapshot'):
483
msg = 'Invalid DeletionPolicy %s' % deletion_policy
484
raise exception.StackValidationFailed(message=msg)
485
elif deletion_policy == 'Snapshot':
486
if not callable(getattr(cls, 'handle_snapshot_delete', None)):
487
msg = 'Snapshot DeletionPolicy not supported'
488
raise exception.StackValidationFailed(message=msg)
492
Delete the resource. Subclasses should provide a handle_delete() method
493
to customise deletion.
495
if (self.action, self.status) == (self.DELETE, self.COMPLETE):
497
# No need to delete if the resource has never been created
498
if self.action is None:
501
initial_state = self.state
503
logger.info('deleting %s' % str(self))
506
self.state_set(self.DELETE, self.IN_PROGRESS)
508
deletion_policy = self.t.get('DeletionPolicy', 'Delete')
509
if deletion_policy == 'Delete':
510
if callable(getattr(self, 'handle_delete', None)):
512
elif deletion_policy == 'Snapshot':
513
if callable(getattr(self, 'handle_snapshot_delete', None)):
514
self.handle_snapshot_delete(initial_state)
515
except Exception as ex:
516
logger.exception('Delete %s', str(self))
517
failure = exception.ResourceFailure(ex)
518
self.state_set(self.DELETE, self.FAILED, str(failure))
521
with excutils.save_and_reraise_exception():
523
self.state_set(self.DELETE, self.FAILED,
526
logger.exception('Error marking resource deletion failed')
528
self.state_set(self.DELETE, self.COMPLETE)
532
Delete the resource and remove it from the database.
540
db_api.resource_get(self.context, self.id).delete()
541
except exception.NotFound:
542
# Don't fail on delete if the db entry has
543
# not been created yet.
548
def resource_id_set(self, inst):
549
self.resource_id = inst
550
if self.id is not None:
552
rs = db_api.resource_get(self.context, self.id)
553
rs.update_and_save({'nova_instance': self.resource_id})
554
except Exception as ex:
555
logger.warn('db error %s' % str(ex))
558
'''Create the resource in the database.'''
560
rs = {'action': self.action,
561
'status': self.status,
562
'status_reason': self.status_reason,
563
'stack_id': self.stack.id,
564
'nova_instance': self.resource_id,
566
'rsrc_metadata': self.metadata,
567
'stack_name': self.stack.name}
569
new_rs = db_api.resource_create(self.context, rs)
572
self.stack.updated_time = datetime.utcnow()
574
except Exception as ex:
575
logger.error('DB error %s' % str(ex))
577
def _add_event(self, action, status, reason):
578
'''Add a state change event to the database.'''
579
ev = event.Event(self.context, self.stack, self,
580
action, status, reason,
581
self.resource_id, self.properties)
585
except Exception as ex:
586
logger.error('DB error %s' % str(ex))
588
def _store_or_update(self, action, status, reason):
591
self.status_reason = reason
593
if self.id is not None:
595
rs = db_api.resource_get(self.context, self.id)
596
rs.update_and_save({'action': self.action,
597
'status': self.status,
598
'status_reason': reason,
599
'nova_instance': self.resource_id})
601
self.stack.updated_time = datetime.utcnow()
602
except Exception as ex:
603
logger.error('DB error %s' % str(ex))
605
# store resource in DB on transition to CREATE_IN_PROGRESS
606
# all other transistions (other than to DELETE_COMPLETE)
607
# should be handled by the update_and_save above..
608
elif (action, status) == (self.CREATE, self.IN_PROGRESS):
611
def _resolve_attribute(self, name):
613
Default implementation; should be overridden by resources that expose
616
:param name: The attribute to resolve
617
:returns: the resource attribute named key
619
# By default, no attributes resolve
622
def state_set(self, action, status, reason="state changed"):
623
if action not in self.ACTIONS:
624
raise ValueError("Invalid action %s" % action)
626
if status not in self.STATUSES:
627
raise ValueError("Invalid status %s" % status)
629
old_state = (self.action, self.status)
630
new_state = (action, status)
631
self._store_or_update(action, status, reason)
633
if new_state != old_state:
634
self._add_event(action, status, reason)
638
'''Returns state, tuple of action, status.'''
639
return (self.action, self.status)
641
def FnGetRefId(self):
643
http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/\
644
intrinsic-function-reference-ref.html
646
if self.resource_id is not None:
647
return unicode(self.resource_id)
649
return unicode(self.name)
651
def FnGetAtt(self, key):
653
http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/\
654
intrinsic-function-reference-getatt.html
657
return self.attributes[key]
659
raise exception.InvalidTemplateAttribute(resource=self.name,
662
def FnBase64(self, data):
664
http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/\
665
intrinsic-function-reference-base64.html
667
return base64.b64encode(data)
669
def handle_update(self, json_snippet=None, tmpl_diff=None, prop_diff=None):
670
raise UpdateReplace(self.name)
672
def metadata_update(self, new_metadata=None):
674
No-op for resources which don't explicitly override this method
677
logger.warning("Resource %s does not implement metadata update" %