heat api/engine : Implement UpdateStack functionality

Implements initial support for UpdateStack, currently
all resources default to delete/create on update.
Ref #171

Change-Id: I3e6e63143d554c21ccdee19879c4dfb8b6e693d7
This commit is contained in:
Steven Hardy 2012-07-17 15:46:49 +01:00
parent 8fb2ad1e28
commit 0191587ac6
17 changed files with 334 additions and 22 deletions

View File

@ -281,10 +281,21 @@ class StackController(object):
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):
"""
@ -302,6 +313,14 @@ class StackController(object):
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
@ -328,7 +347,7 @@ class StackController(object):
try:
res = rpc.call(con, 'engine',
{'method': 'create_stack',
{'method': engine_action[action],
'args': {'stack_name': req.params['StackName'],
'template': stack,
'params': stack_parms,
@ -336,8 +355,7 @@ class StackController(object):
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):
"""

View File

@ -107,6 +107,10 @@ def stack_create(context, values):
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)

View File

@ -154,6 +154,27 @@ def stack_create(context, values):
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:

View File

@ -62,6 +62,9 @@ class AutoScalingGroup(Resource):
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']

View File

@ -73,6 +73,9 @@ class CloudWatchAlarm(Resource):
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)

View File

@ -49,6 +49,9 @@ class ElasticIp(Resource):
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

View File

@ -258,6 +258,9 @@ class Instance(resources.Resource):
('nova reported unexpected',
self.name, server.status))
def handle_update(self):
return self.UPDATE_REPLACE
def validate(self):
'''
Validate any of the provided params

View File

@ -312,6 +312,9 @@ class LoadBalancer(stack.Stack):
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.

View File

@ -122,6 +122,46 @@ class EngineManager(manager.Manager):
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

View File

@ -227,13 +227,18 @@ class Template(object):
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')
@ -290,21 +295,26 @@ class Stack(object):
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())
'''
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,
}
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
@ -332,6 +342,10 @@ class Stack(object):
'''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
@ -394,7 +408,7 @@ class Stack(object):
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'
@ -424,6 +438,116 @@ class Stack(object):
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.

View File

@ -100,6 +100,7 @@ class Timestamp(object):
class Resource(object):
# Status strings
CREATE_IN_PROGRESS = 'IN_PROGRESS'
CREATE_FAILED = 'CREATE_FAILED'
CREATE_COMPLETE = 'CREATE_COMPLETE'
@ -110,6 +111,12 @@ class Resource(object):
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
@ -153,6 +160,21 @@ class Resource(object):
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
@ -232,6 +254,43 @@ class Resource(object):
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))
@ -375,6 +434,10 @@ class Resource(object):
'''
return base64.b64encode(data)
def handle_update(self):
raise NotImplementedError("Update not implemented for Resource %s"
% type(self))
class GenericResource(Resource):
properties_schema = {}
@ -382,3 +445,7 @@ class GenericResource(Resource):
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'])

View File

@ -66,6 +66,9 @@ class SecurityGroup(Resource):
# unexpected error
raise
def handle_update(self):
return self.UPDATE_REPLACE
def handle_delete(self):
if self.instance_id is not None:
try:

View File

@ -79,6 +79,9 @@ class Stack(Resource):
self.create_with_template(template)
def handle_update(self):
return self.UPDATE_REPLACE
def handle_delete(self):
try:
stack = self.nested()

View File

@ -56,6 +56,9 @@ class User(Resource):
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))
@ -137,6 +140,9 @@ class AccessKey(Resource):
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:

View File

@ -46,6 +46,9 @@ class Volume(Resource):
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)
@ -87,6 +90,9 @@ class VolumeAttachment(Resource):
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']

View File

@ -43,6 +43,8 @@ class WaitConditionHandle(resources.Resource):
self.stack.id,
self.name)
def handle_update(self):
return self.UPDATE_REPLACE
WAIT_STATUSES = (
WAITING,
@ -111,6 +113,9 @@ class WaitCondition(resources.Resource):
if status != SUCCESS:
raise exception.Error(reason)
def handle_update(self):
return self.UPDATE_REPLACE
def FnGetAtt(self, key):
res = None
if key == 'Data':

View File

@ -369,7 +369,7 @@ class StackTest(unittest.TestCase):
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)