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:
parent
7406a33486
commit
6ce61eee30
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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({}),
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue