Implement adopt-stack

Create a stack using user specified, already existing external
resources. The user specifies what resources to adopt using the
same serialization format as the one produced by abandon-stack.

Change-Id: I87b6d94780651143cd61fe95ee083e4310ad59c8
Implements: blueprint adopt-stack
This commit is contained in:
Vijendar Komalla 2014-01-02 10:47:07 -06:00 committed by Gerrit Code Review
parent 7406a33486
commit 6ce61eee30
9 changed files with 321 additions and 15 deletions

View File

@ -48,6 +48,11 @@ def extract_args(params):
' %(name)s : %(value)s') %
dict(name=api.PARAM_DISABLE_ROLLBACK,
value=disable_rollback))
if api.PARAM_ADOPT_STACK_DATA in params:
kwargs[api.PARAM_ADOPT_STACK_DATA] = params.get(
api.PARAM_ADOPT_STACK_DATA)
return kwargs

View File

@ -50,9 +50,9 @@ logger = logging.getLogger(__name__)
class Stack(collections.Mapping):
ACTIONS = (CREATE, DELETE, UPDATE, ROLLBACK, SUSPEND, RESUME
ACTIONS = (CREATE, DELETE, UPDATE, ROLLBACK, SUSPEND, RESUME, ADOPT
) = ('CREATE', 'DELETE', 'UPDATE', 'ROLLBACK', 'SUSPEND',
'RESUME')
'RESUME', 'ADOPT')
STATUSES = (IN_PROGRESS, FAILED, COMPLETE
) = ('IN_PROGRESS', 'FAILED', 'COMPLETE')
@ -69,7 +69,8 @@ class Stack(collections.Mapping):
def __init__(self, context, stack_name, tmpl, env=None,
stack_id=None, action=None, status=None,
status_reason='', timeout_mins=60, resolve_data=True,
disable_rollback=True, parent_resource=None, owner_id=None):
disable_rollback=True, parent_resource=None, owner_id=None,
adopt_stack_data=None):
'''
Initialise from a context, name, Template object and (optionally)
Environment object. The database ID may also be initialised, if the
@ -98,6 +99,7 @@ class Stack(collections.Mapping):
self._resources = None
self._dependencies = None
self._access_allowed_handlers = {}
self.adopt_stack_data = adopt_stack_data
resources.initialise()
@ -407,6 +409,13 @@ class Stack(collections.Mapping):
post_func=rollback)
creator(timeout=self.timeout_secs())
def _adopt_kwargs(self, resource):
data = self.adopt_stack_data
if not data or not data.get('resources'):
return {'resource_data': None}
return {'resource_data': data['resources'].get(resource.name)}
@scheduler.wrappertask
def stack_task(self, action, reverse=False, post_func=None):
'''
@ -424,7 +433,11 @@ class Stack(collections.Mapping):
action_l = action.lower()
handle = getattr(r, '%s' % action_l)
return handle()
# If a local _$action_kwargs function exists, call it to get the
# action specific argument list, otherwise an empty arg list
handle_kwargs = getattr(self,
'_%s_kwargs' % action_l, lambda x: {})
return handle(**handle_kwargs(r))
action_task = scheduler.DependencyTaskGroup(self.dependencies,
resource_action,
@ -466,6 +479,22 @@ class Stack(collections.Mapping):
else:
return None
def adopt(self):
'''
Adopt a stack (create stack with all the existing resources).
'''
def rollback():
if not self.disable_rollback and self.state == (self.ADOPT,
self.FAILED):
self.delete(action=self.ROLLBACK)
creator = scheduler.TaskRunner(
self.stack_task,
action=self.ADOPT,
reverse=False,
post_func=rollback)
creator(timeout=self.timeout_secs())
def update(self, newstack):
'''
Compare the current stack with newstack,

View File

@ -114,9 +114,9 @@ class SupportStatus(object):
class Resource(object):
ACTIONS = (INIT, CREATE, DELETE, UPDATE, ROLLBACK, SUSPEND, RESUME
ACTIONS = (INIT, CREATE, DELETE, UPDATE, ROLLBACK, SUSPEND, RESUME, ADOPT
) = ('INIT', 'CREATE', 'DELETE', 'UPDATE', 'ROLLBACK',
'SUSPEND', 'RESUME')
'SUSPEND', 'RESUME', 'ADOPT')
STATUSES = (IN_PROGRESS, FAILED, COMPLETE
) = ('IN_PROGRESS', 'FAILED', 'COMPLETE')
@ -399,7 +399,7 @@ class Resource(object):
def heat(self):
return self.stack.clients.heat()
def _do_action(self, action, pre_func=None):
def _do_action(self, action, pre_func=None, resource_data=None):
'''
Perform a transition to a new state via a specified action
action should be e.g self.CREATE, self.UPDATE etc, we set
@ -428,7 +428,8 @@ class Resource(object):
handle_data = None
if callable(handle):
handle_data = handle()
handle_data = (handle(resource_data) if resource_data else
handle())
yield
if callable(check):
while not check(handle_data):
@ -487,6 +488,40 @@ class Resource(object):
for r in db_api.resource_data_get_all(self))
}
def adopt(self, resource_data):
'''
Adopt the existing resource. Resource subclasses can provide
a handle_adopt() method to customise adopt.
'''
return self._do_action(self.ADOPT, resource_data=resource_data)
def handle_adopt(self, resource_data=None):
resource_id, data, metadata = self._get_resource_info(resource_data)
if not resource_id:
exc = Exception(_('Resource ID was not provided.'))
failure = exception.ResourceFailure(exc, self)
raise failure
# set resource id
self.resource_id_set(resource_id)
# save the resource data
if data and isinstance(data, dict):
for key, value in data.iteritems():
db_api.resource_data_set(self, key, value)
# save the resource metadata
self.metadata = metadata
def _get_resource_info(self, resource_data):
if not resource_data:
return None, None, None
return (resource_data.get('resource_id'),
resource_data.get('resource_data'),
resource_data.get('metadata'))
def update(self, after, before=None, prev_resource=None):
'''
update the resource. Subclasses should provide a handle_update() method
@ -508,7 +543,8 @@ class Resource(object):
return
if (self.action, self.status) in ((self.CREATE, self.IN_PROGRESS),
(self.UPDATE, self.IN_PROGRESS)):
(self.UPDATE, self.IN_PROGRESS),
(self.ADOPT, self.IN_PROGRESS)):
exc = Exception(_('Resource update already requested'))
raise exception.ResourceFailure(exc, self, action)
@ -762,7 +798,8 @@ class Resource(object):
# store resource in DB on transition to CREATE_IN_PROGRESS
# all other transistions (other than to DELETE_COMPLETE)
# should be handled by the update_and_save above..
elif (action, status) == (self.CREATE, self.IN_PROGRESS):
elif (action, status) in [(self.CREATE, self.IN_PROGRESS),
(self.ADOPT, self.IN_PROGRESS)]:
self._store()
def _resolve_attribute(self, name):

View File

@ -328,9 +328,14 @@ class EngineService(service.Service):
logger.info(_('template is %s') % template)
def _stack_create(stack):
# Create the stack, and create the periodic task if successful
stack.create()
if stack.action == stack.CREATE and stack.status == stack.COMPLETE:
# Create/Adopt a stack, and create the periodic task if successful
if stack.adopt_stack_data:
stack.adopt()
else:
stack.create()
if (stack.action in (stack.CREATE, stack.ADOPT)
and stack.status == stack.COMPLETE):
# Schedule a periodic watcher task for this stack
self._start_watch_task(stack.id, cnxt)
else:

View File

@ -15,9 +15,9 @@
ENGINE_TOPIC = 'engine'
PARAM_KEYS = (
PARAM_TIMEOUT, PARAM_DISABLE_ROLLBACK
PARAM_TIMEOUT, PARAM_DISABLE_ROLLBACK, PARAM_ADOPT_STACK_DATA
) = (
'timeout_mins', 'disable_rollback'
'timeout_mins', 'disable_rollback', 'adopt_stack_data'
)
STACK_KEYS = (

View File

@ -51,6 +51,15 @@ class EngineApiTest(HeatTestCase):
args = api.extract_args({})
self.assertNotIn('timeout_mins', args)
def test_adopt_stack_data_extract_present(self):
p = {'adopt_stack_data': {'Resources': {}}}
args = api.extract_args(p)
self.assertTrue(args.get('adopt_stack_data'))
def test_adopt_stack_data_extract_not_present(self):
args = api.extract_args({})
self.assertNotIn('adopt_stack_data', args)
def test_disable_rollback_extract_true(self):
args = api.extract_args({'disable_rollback': True})
self.assertIn('disable_rollback', args)

View File

@ -307,6 +307,57 @@ class StackCreateTest(HeatTestCase):
self.assertTrue(stack['WebServer'].resource_id > 0)
self.assertNotEqual(stack['WebServer'].ipaddress, '0.0.0.0')
def test_wordpress_single_instance_stack_adopt(self):
t = template_format.parse(wp_template)
template = parser.Template(t)
ctx = utils.dummy_context()
adopt_data = {
'resources': {
'WebServer': {
'resource_id': 'test-res-id'
}
}
}
stack = parser.Stack(ctx,
'test_stack',
template,
adopt_stack_data=adopt_data)
setup_mocks(self.m, stack)
self.m.ReplayAll()
stack.store()
stack.adopt()
self.assertIsNotNone(stack['WebServer'])
self.assertEqual('test-res-id', stack['WebServer'].resource_id)
self.assertEqual((stack.ADOPT, stack.COMPLETE), stack.state)
def test_wordpress_single_instance_stack_adopt_fail(self):
t = template_format.parse(wp_template)
template = parser.Template(t)
ctx = utils.dummy_context()
adopt_data = {
'resources': {
'WebServer1': {
'resource_id': 'test-res-id'
}
}
}
stack = parser.Stack(ctx,
'test_stack',
template,
adopt_stack_data=adopt_data)
setup_mocks(self.m, stack)
self.m.ReplayAll()
stack.store()
stack.adopt()
self.assertIsNotNone(stack['WebServer'])
expected = ('Resource adopt failed: Exception: Resource ID was not'
' provided.')
self.assertEqual(expected, stack.status_reason)
self.assertEqual((stack.ADOPT, stack.FAILED), stack.state)
def test_wordpress_single_instance_stack_delete(self):
ctx = utils.dummy_context()
stack = get_wordpress_stack('test_stack', ctx)

View File

@ -1203,6 +1203,91 @@ class StackTest(HeatTestCase):
self.assertEqual(self.stack.state,
(parser.Stack.DELETE, parser.Stack.FAILED))
@utils.stack_delete_after
def test_adopt_stack(self):
adopt_data = '''{
"action": "CREATE",
"status": "COMPLETE",
"name": "my-test-stack-name",
"resources": {
"foo": {
"status": "COMPLETE",
"name": "foo",
"resource_data": {},
"metadata": {},
"resource_id": "test-res-id",
"action": "CREATE",
"type": "GenericResourceType"
}
}
}'''
tmpl = template.Template({
'Resources': {
'foo': {'Type': 'GenericResourceType'},
}
})
self.stack = parser.Stack(utils.dummy_context(), 'test_stack',
tmpl,
adopt_stack_data=json.loads(adopt_data))
self.stack.store()
self.stack.adopt()
res = self.stack['foo']
self.assertEqual(u'test-res-id', res.resource_id)
self.assertEqual('foo', res.name)
self.assertEqual('COMPLETE', res.status)
self.assertEqual('ADOPT', res.action)
self.assertEqual((self.stack.ADOPT, self.stack.COMPLETE),
self.stack.state)
@utils.stack_delete_after
def test_adopt_stack_fails(self):
adopt_data = '''{
"action": "CREATE",
"status": "COMPLETE",
"name": "my-test-stack-name",
"resources": {}
}'''
tmpl = template.Template({
'Resources': {
'foo': {'Type': 'GenericResourceType'},
}
})
self.stack = parser.Stack(utils.dummy_context(), 'test_stack',
tmpl,
adopt_stack_data=json.loads(adopt_data))
self.stack.store()
self.stack.adopt()
self.assertEqual((self.stack.ADOPT, self.stack.FAILED),
self.stack.state)
expected = ('Resource adopt failed: Exception: Resource ID was not'
' provided.')
self.assertEqual(expected, self.stack.status_reason)
@utils.stack_delete_after
def test_adopt_stack_rollback(self):
adopt_data = '''{
"name": "my-test-stack-name",
"resources": {}
}'''
tmpl = template.Template({
'Resources': {
'foo': {'Type': 'GenericResourceType'},
}
})
self.stack = parser.Stack(utils.dummy_context(),
'test_stack',
tmpl,
disable_rollback=False,
adopt_stack_data=json.loads(adopt_data))
self.stack.store()
self.stack.adopt()
self.assertEqual((self.stack.ROLLBACK, self.stack.COMPLETE),
self.stack.state)
@utils.stack_delete_after
def test_update_badstate(self):
self.stack = parser.Stack(self.ctx, 'test_stack', parser.Template({}),

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import itertools
import uuid
@ -654,6 +655,90 @@ class ResourceTest(HeatTestCase):
'Test::Resource::resource'))
class ResourceAdoptTest(HeatTestCase):
def setUp(self):
super(ResourceAdoptTest, self).setUp()
utils.setup_dummy_db()
resource._register_class('GenericResourceType',
generic_rsrc.GenericResource)
def test_adopt_resource_success(self):
adopt_data = '{}'
tmpl = template.Template({
'Resources': {
'foo': {'Type': 'GenericResourceType'},
}
})
self.stack = parser.Stack(utils.dummy_context(), 'test_stack',
tmpl,
stack_id=str(uuid.uuid4()),
adopt_stack_data=json.loads(adopt_data))
res = self.stack['foo']
res_data = {
"status": "COMPLETE",
"name": "foo",
"resource_data": {},
"metadata": {},
"resource_id": "test-res-id",
"action": "CREATE",
"type": "GenericResourceType"
}
adopt = scheduler.TaskRunner(res.adopt, res_data)
adopt()
self.assertEqual({}, res.metadata)
self.assertEqual((res.ADOPT, res.COMPLETE), res.state)
def test_adopt_with_resource_data_and_metadata(self):
adopt_data = '{}'
tmpl = template.Template({
'Resources': {
'foo': {'Type': 'GenericResourceType'},
}
})
self.stack = parser.Stack(utils.dummy_context(), 'test_stack',
tmpl,
stack_id=str(uuid.uuid4()),
adopt_stack_data=json.loads(adopt_data))
res = self.stack['foo']
res_data = {
"status": "COMPLETE",
"name": "foo",
"resource_data": {"test-key": "test-value"},
"metadata": {"os_distro": "test-distro"},
"resource_id": "test-res-id",
"action": "CREATE",
"type": "GenericResourceType"
}
adopt = scheduler.TaskRunner(res.adopt, res_data)
adopt()
self.assertEqual("test-value",
db_api.resource_data_get(res, "test-key"))
self.assertEqual({"os_distro": "test-distro"}, res.metadata)
self.assertEqual((res.ADOPT, res.COMPLETE), res.state)
def test_adopt_resource_missing(self):
adopt_data = '''{
"action": "CREATE",
"status": "COMPLETE",
"name": "my-test-stack-name",
"resources": {}
}'''
tmpl = template.Template({
'Resources': {
'foo': {'Type': 'GenericResourceType'},
}
})
self.stack = parser.Stack(utils.dummy_context(), 'test_stack',
tmpl,
stack_id=str(uuid.uuid4()),
adopt_stack_data=json.loads(adopt_data))
res = self.stack['foo']
adopt = scheduler.TaskRunner(res.adopt, None)
self.assertRaises(exception.ResourceFailure, adopt)
expected = 'Exception: Resource ID was not provided.'
self.assertEqual(expected, res.status_reason)
class ResourceDependenciesTest(HeatTestCase):
def setUp(self):
super(ResourceDependenciesTest, self).setUp()