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:
parent
8fb2ad1e28
commit
0191587ac6
@ -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):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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']
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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'])
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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']
|
||||
|
@ -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':
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user