return None
+ CREATE_OR_UPDATE_ACTION = (
+ CREATE_STACK, UPDATE_STACK
+ ) = (
+ "CreateStack", "UpdateStack")
+
def create(self, req):
+ return self.create_or_update(req, self.CREATE_STACK)
+
+ def update(self, req):
+ return self.create_or_update(req, self.UPDATE_STACK)
+
+ def create_or_update(self, req, action=None):
"""
- Implements CreateStack API action
- Create stack as defined in template file
+ Implements CreateStack and UpdateStack API actions
+ Create or update stack as defined in template file
"""
def extract_args(params):
"""
return result
+ if action not in self.CREATE_OR_UPDATE_ACTION:
+ msg = _("Unexpected action %s" % action)
+ # This should not happen, so return HeatInternalFailureError
+ return exception.HeatInternalFailureError(detail=msg)
+
+ engine_action = {self.CREATE_STACK: "create_stack",
+ self.UPDATE_STACK: "update_stack"}
+
con = req.context
# Extract the stack input parameters
try:
res = rpc.call(con, 'engine',
- {'method': 'create_stack',
+ {'method': engine_action[action],
'args': {'stack_name': req.params['StackName'],
'template': stack,
'params': stack_parms,
except rpc_common.RemoteError as ex:
return self._remote_error(ex)
- return self._format_response('CreateStack',
- self._stackid_addprefix(res))
+ return self._format_response(action, self._stackid_addprefix(res))
def get_template(self, req):
"""
return IMPL.stack_create(context, values)
+def stack_update(context, stack_id, values):
+ return IMPL.stack_update(context, stack_id, values)
+
+
def stack_delete(context, stack_id):
return IMPL.stack_delete(context, stack_id)
return stack_ref
+def stack_update(context, stack_id, values):
+ stack = stack_get(context, stack_id)
+
+ if not stack:
+ raise NotFound('Attempt to update a stack with id: %s %s' %
+ (stack_id, 'that does not exist'))
+
+ old_template_id = stack.raw_template_id
+
+ stack.update(values)
+ stack.save()
+
+ # When the raw_template ID changes, we delete the old template
+ # after storing the new template ID
+ if stack.raw_template_id != old_template_id:
+ session = Session.object_session(stack)
+ rt = raw_template_get(context, old_template_id)
+ session.delete(rt)
+ session.flush()
+
+
def stack_delete(context, stack_id):
s = stack_get(context, stack_id)
if not s:
self.adjust(int(self.properties['MinSize']),
adjustment_type='ExactCapacity')
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def handle_delete(self):
if self.instance_id is not None:
conf = self.properties['LaunchConfigurationName']
wr = db_api.watch_rule_create(self.context, wr_values)
self.instance_id = wr.id
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def handle_delete(self):
try:
db_api.watch_rule_delete(self.context, self.name)
self.ipaddress = ips.ip
self.instance_id_set(ips.id)
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def validate(self):
'''
Validate the ip address here
('nova reported unexpected',
self.name, server.status))
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def validate(self):
'''
Validate any of the provided params
def FnGetRefId(self):
return unicode(self.name)
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def FnGetAtt(self, key):
'''
We don't really support any of these yet.
return {'StackName': stack.name, 'StackId': stack.id}
+ def update_stack(self, context, stack_name, template, params, args):
+ """
+ The update_stack method updates an existing stack based on the
+ provided template and parameters.
+ Note that at this stage the template has already been fetched from the
+ heat-api process if using a template-url.
+ arg1 -> RPC context.
+ arg2 -> Name of the stack you want to create.
+ arg3 -> Template of stack you want to create.
+ arg4 -> Stack Input Params
+ arg4 -> Request parameters/args passed from API
+ """
+ logger.info('template is %s' % template)
+
+ auth.authenticate(context)
+
+ # Get the database representation of the existing stack
+ db_stack = db_api.stack_get_by_name(None, stack_name)
+ if not db_stack:
+ return {'Error': 'No stack exists with that name.'}
+
+ current_stack = parser.Stack.load(context, db_stack.id)
+
+ # Now parse the template and any parameters for the updated
+ # stack definition.
+ tmpl = parser.Template(template)
+ template_params = parser.Parameters(stack_name, tmpl, params)
+ common_params = api.extract_args(args)
+
+ updated_stack = parser.Stack(context, stack_name, tmpl,
+ template_params, **common_params)
+
+ response = updated_stack.validate()
+ if response['Description'] != 'Successfully validated':
+ return response
+
+ greenpool.spawn_n(current_stack.update, updated_stack)
+
+ return {'StackName': current_stack.name, 'StackId': current_stack.id}
+
def validate_template(self, context, template, params):
"""
The validate_template method uses the stack parser to check
class Stack(object):
- IN_PROGRESS = 'IN_PROGRESS'
+ CREATE_IN_PROGRESS = 'CREATE_IN_PROGRESS'
CREATE_FAILED = 'CREATE_FAILED'
CREATE_COMPLETE = 'CREATE_COMPLETE'
+
DELETE_IN_PROGRESS = 'DELETE_IN_PROGRESS'
DELETE_FAILED = 'DELETE_FAILED'
DELETE_COMPLETE = 'DELETE_COMPLETE'
+ UPDATE_IN_PROGRESS = 'UPDATE_IN_PROGRESS'
+ UPDATE_COMPLETE = 'UPDATE_COMPLETE'
+ UPDATE_FAILED = 'UPDATE_FAILED'
+
created_time = resources.Timestamp(db_api.stack_get, 'created_at')
updated_time = resources.Timestamp(db_api.stack_get, 'updated_at')
return stack
def store(self, owner=None):
- '''Store the stack in the database and return its ID'''
- if self.id is None:
- new_creds = db_api.user_creds_create(self.context.to_dict())
-
- s = {
- 'name': self.name,
- 'raw_template_id': self.t.store(),
- 'parameters': self.parameters.user_parameters(),
- 'owner_id': owner and owner.id,
- 'user_creds_id': new_creds.id,
- 'username': self.context.username,
- 'status': self.state,
- 'status_reason': self.state_description,
- 'timeout': self.timeout_mins,
- }
+ '''
+ Store the stack in the database and return its ID
+ If self.id is set, we update the existing stack
+ '''
+ new_creds = db_api.user_creds_create(self.context.to_dict())
+
+ s = {
+ 'name': self.name,
+ 'raw_template_id': self.t.store(),
+ 'parameters': self.parameters.user_parameters(),
+ 'owner_id': owner and owner.id,
+ 'user_creds_id': new_creds.id,
+ 'username': self.context.username,
+ 'status': self.state,
+ 'status_reason': self.state_description,
+ 'timeout': self.timeout_mins,
+ }
+ if self.id:
+ db_api.stack_update(self.context, self.id, s)
+ else:
new_s = db_api.stack_create(self.context, s)
self.id = new_s.id
'''Get the resource with the specified name.'''
return self.resources[key]
+ def __setitem__(self, key, value):
+ '''Set the resource with the specified name to a specific value'''
+ self.resources[key] = value
+
def __contains__(self, key):
'''Determine whether the stack contains the specified resource'''
return key in self.resources
Creation will fail if it exceeds the specified timeout. The default is
60 minutes, set in the constructor
'''
- self.state_set(self.IN_PROGRESS, 'Stack creation started')
+ self.state_set(self.CREATE_IN_PROGRESS, 'Stack creation started')
stack_status = self.CREATE_COMPLETE
reason = 'Stack successfully created'
self.state_set(stack_status, reason)
+ def update(self, newstack):
+ '''
+ Compare the current stack with newstack,
+ and where necessary create/update/delete the resources until
+ this stack aligns with newstack.
+
+ Note update of existing stack resources depends on update
+ being implemented in the underlying resource types
+
+ Update will fail if it exceeds the specified timeout. The default is
+ 60 minutes, set in the constructor
+ '''
+ self.state_set(self.UPDATE_IN_PROGRESS, 'Stack update started')
+
+ # Now make the resources match the new stack definition
+ failures = []
+ with eventlet.Timeout(self.timeout_mins * 60) as tmo:
+ try:
+ # First delete any resources which are not in newstack
+ for res in self:
+ if not res.name in newstack.keys():
+ logger.debug("resource %s not found in updated stack"
+ % res.name + " definition, deleting")
+ result = res.destroy()
+ if result:
+ failures.append('Resource %s delete failed'
+ % res.name)
+ else:
+ del self.resources[res.name]
+
+ # Then create any which are defined in newstack but not self
+ for res in newstack:
+ if not res.name in self.keys():
+ logger.debug("resource %s not found in current stack"
+ % res.name + " definition, adding")
+ res.stack = self
+ self[res.name] = res
+ result = self[res.name].create()
+ if result:
+ failures.append('Resource %s create failed'
+ % res.name)
+
+ # Now (the hard part :) update existing resources
+ # The Resource base class allows equality-test of resources,
+ # based on the parsed template snippet for the resource.
+ # If this test fails, we call the underlying resource.update
+ #
+ # FIXME : Implement proper update logic for the resources
+ # AWS define three update strategies, applied depending
+ # on the resource and what is being updated within a
+ # resource :
+ # - Update with no interruption
+ # - Update with some interruption
+ # - Update requires replacement
+ #
+ # Currently all resource have a default handle_update method
+ # which returns "requires replacement" (res.UPDATE_REPLACE)
+ for res in newstack:
+ if self[res.name] != res:
+ # Can fail if underlying resource class does not
+ # implement update logic or update requires replacement
+ retval = self[res.name].update(res.parsed_template())
+ if retval == self[res.name].UPDATE_REPLACE:
+ logger.info("Resource %s for stack %s" %
+ (res.name, self.name) +
+ " update requires replacement")
+ # Resource requires replacement for update
+ result = self[res.name].destroy()
+ if result:
+ failures.append('Resource %s delete failed'
+ % res.name)
+ else:
+ res.stack = self
+ self[res.name] = res
+ result = self[res.name].create()
+ if result:
+ failures.append('Resource %s create failed'
+ % res.name)
+ else:
+ logger.warning("Cannot update resource %s," %
+ res.name + " reason %s" % retval)
+ failures.append('Resource %s update failed'
+ % res.name)
+
+ # Set stack status values
+ if not failures:
+ # flip the template & parameters to the newstack values
+ self.t = newstack.t
+ self.parameters = newstack.parameters
+ self.outputs = self.resolve_static_data(self.t[OUTPUTS])
+ self.dependencies = self._get_dependencies(
+ self.resources.itervalues())
+ self.store()
+
+ stack_status = self.UPDATE_COMPLETE
+ reason = 'Stack successfully updated'
+ else:
+ stack_status = self.UPDATE_FAILED
+ reason = ",".join(failures)
+
+ except eventlet.Timeout as t:
+ if t is tmo:
+ stack_status = self.UPDATE_FAILED
+ reason = 'Timed out waiting for %s' % str(res)
+ else:
+ # not my timeout
+ raise
+
+ self.state_set(stack_status, reason)
+
def delete(self):
'''
Delete all of the resources, and then the stack itself.
class Resource(object):
+ # Status strings
CREATE_IN_PROGRESS = 'IN_PROGRESS'
CREATE_FAILED = 'CREATE_FAILED'
CREATE_COMPLETE = 'CREATE_COMPLETE'
UPDATE_FAILED = 'UPDATE_FAILED'
UPDATE_COMPLETE = 'UPDATE_COMPLETE'
+ # Status values, returned from subclasses to indicate update method
+ UPDATE_REPLACE = 'UPDATE_REPLACE'
+ UPDATE_INTERRUPTION = 'UPDATE_INTERRUPTION'
+ UPDATE_NO_INTERRUPTION = 'UPDATE_NO_INTERRUPTION'
+ UPDATE_NOT_IMPLEMENTED = 'UPDATE_NOT_IMPLEMENTED'
+
# If True, this resource must be created before it can be referenced.
strict_dependency = True
self._nova = {}
self._keystone = None
+ def __eq__(self, other):
+ '''Allow == comparison of two resources'''
+ # For the purposes of comparison, we declare two resource objects
+ # equal if their parsed_templates are the same
+ if isinstance(other, Resource):
+ return self.parsed_template() == other.parsed_template()
+ return NotImplemented
+
+ def __ne__(self, other):
+ '''Allow != comparison of two resources'''
+ result = self.__eq__(other)
+ if result is NotImplemented:
+ return result
+ return not result
+
def parsed_template(self, section=None, default={}):
'''
Return the parsed template data for the resource. May be limited to
else:
self.state_set(self.CREATE_COMPLETE)
+ def update(self, json_snippet=None):
+ '''
+ update the resource. Subclasses should provide a handle_update() method
+ to customise update, the base-class handle_update will fail by default.
+ '''
+ if self.state in (self.CREATE_IN_PROGRESS, self.UPDATE_IN_PROGRESS):
+ return 'Resource update already requested'
+
+ if not json_snippet:
+ return 'Must specify json snippet for resource update!'
+
+ logger.info('updating %s' % str(self))
+
+ result = self.UPDATE_NOT_IMPLEMENTED
+ try:
+ self.state_set(self.UPDATE_IN_PROGRESS)
+ self.t = self.stack.resolve_static_data(json_snippet)
+ self.properties = checkeddict.Properties(self.name,
+ self.properties_schema)
+ self.calculate_properties()
+ self.properties.validate()
+ if callable(getattr(self, 'handle_update', None)):
+ result = self.handle_update()
+ except Exception as ex:
+ logger.exception('update %s : %s' % (str(self), str(ex)))
+ self.state_set(self.UPDATE_FAILED, str(ex))
+ return str(ex)
+ else:
+ # If resource was updated (with or without interruption),
+ # then we set the resource to UPDATE_COMPLETE
+ if not result == self.UPDATE_REPLACE:
+ self.state_set(self.UPDATE_COMPLETE)
+ return result
+
+ def validate(self):
+ logger.info('Validating %s' % str(self))
+
def validate(self):
logger.info('Validating %s' % str(self))
'''
return base64.b64encode(data)
+ def handle_update(self):
+ raise NotImplementedError("Update not implemented for Resource %s"
+ % type(self))
+
class GenericResource(Resource):
properties_schema = {}
def handle_create(self):
logger.warning('Creating generic resource (Type "%s")' %
self.t['Type'])
+
+ def handle_update(self):
+ logger.warning('Updating generic resource (Type "%s")' %
+ self.t['Type'])
# unexpected error
raise
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def handle_delete(self):
if self.instance_id is not None:
try:
self.create_with_template(template)
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def handle_delete(self):
try:
stack = self.nested()
enabled=True)
self.instance_id_set(user.id)
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def handle_delete(self):
try:
user = self.keystone().users.get(DummyId(self.instance_id))
self.instance_id_set(cred.access)
self._secret = cred.secret
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def handle_delete(self):
user = self._user_from_name(self.properties['UserName'])
if user and self.instance_id:
else:
raise exception.Error(vol.status)
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def handle_delete(self):
if self.instance_id is not None:
vol = self.nova('volume').volumes.get(self.instance_id)
else:
raise exception.Error(vol.status)
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def handle_delete(self):
server_id = self.properties['InstanceId']
volume_id = self.properties['VolumeId']
self.stack.id,
self.name)
+ def handle_update(self):
+ return self.UPDATE_REPLACE
WAIT_STATUSES = (
WAITING,
if status != SUCCESS:
raise exception.Error(reason)
+ def handle_update(self):
+ return self.UPDATE_REPLACE
+
def FnGetAtt(self, key):
res = None
if key == 'Data':
self.assertEqual(stack.updated_time, None)
stack.store()
stored_time = stack.updated_time
- stack.state_set(stack.IN_PROGRESS, 'testing')
+ stack.state_set(stack.CREATE_IN_PROGRESS, 'testing')
self.assertNotEqual(stack.updated_time, None)
self.assertNotEqual(stack.updated_time, stored_time)