Rework orchestration to add update preview
With a slight rework of basic resource and proxy a change can be done in orchestrate to throw away useless StackPreview class and add possibility to create/update stack with preview (dry-run) using a single regular Stack class. Additionally stack.abandon is added Task: 28128 Change-Id: I9a0a2a389be04a5cbcc3dd085ef830c58af3c1d0
This commit is contained in:
parent
8274409c9e
commit
aea3ea2ad1
|
@ -28,10 +28,8 @@ class Proxy(proxy.Proxy):
|
|||
def create_stack(self, preview=False, **attrs):
|
||||
"""Create a new stack from attributes
|
||||
|
||||
:param bool preview: When ``True``, returns
|
||||
an :class:`~openstack.orchestration.v1.stack.StackPreview` object,
|
||||
otherwise an :class:`~openstack.orchestration.v1.stack.Stack`
|
||||
object.
|
||||
:param bool preview: When ``True``, a preview endpoint will be used to
|
||||
verify the template
|
||||
*Default: ``False``*
|
||||
:param dict attrs: Keyword arguments which will be used to create
|
||||
a :class:`~openstack.orchestration.v1.stack.Stack`,
|
||||
|
@ -40,8 +38,8 @@ class Proxy(proxy.Proxy):
|
|||
:returns: The results of stack creation
|
||||
:rtype: :class:`~openstack.orchestration.v1.stack.Stack`
|
||||
"""
|
||||
res_type = _stack.StackPreview if preview else _stack.Stack
|
||||
return self._create(res_type, **attrs)
|
||||
base_path = None if not preview else '/stacks/preview'
|
||||
return self._create(_stack.Stack, base_path=base_path, **attrs)
|
||||
|
||||
def find_stack(self, name_or_id, ignore_missing=True):
|
||||
"""Find a single stack
|
||||
|
@ -80,7 +78,7 @@ class Proxy(proxy.Proxy):
|
|||
"""
|
||||
return self._get(_stack.Stack, stack)
|
||||
|
||||
def update_stack(self, stack, **attrs):
|
||||
def update_stack(self, stack, preview=False, **attrs):
|
||||
"""Update a stack
|
||||
|
||||
:param stack: The value can be the ID of a stack or a
|
||||
|
@ -93,7 +91,8 @@ class Proxy(proxy.Proxy):
|
|||
:raises: :class:`~openstack.exceptions.ResourceNotFound`
|
||||
when no resource can be found.
|
||||
"""
|
||||
return self._update(_stack.Stack, stack, **attrs)
|
||||
res = self._get_resource(_stack.Stack, stack, **attrs)
|
||||
return res.update(self, preview)
|
||||
|
||||
def delete_stack(self, stack, ignore_missing=True):
|
||||
"""Delete a stack
|
||||
|
@ -128,6 +127,16 @@ class Proxy(proxy.Proxy):
|
|||
|
||||
stk_obj.check(self)
|
||||
|
||||
def abandon_stack(self, stack):
|
||||
"""Abandon a stack's without deleting it's resources
|
||||
|
||||
:param stack: The value can be either the ID of a stack or an instance
|
||||
of :class:`~openstack.orchestration.v1.stack.Stack`.
|
||||
:returns: ``None``
|
||||
"""
|
||||
res = self._get_resource(_stack.Stack, stack)
|
||||
return res.abandon(self)
|
||||
|
||||
def get_stack_template(self, stack):
|
||||
"""Get template used by a stack
|
||||
|
||||
|
|
|
@ -29,6 +29,9 @@ class Stack(resource.Resource):
|
|||
allow_delete = True
|
||||
|
||||
# Properties
|
||||
#: A list of resource objects that will be added if a stack update
|
||||
# is performed.
|
||||
added = resource.Body('added')
|
||||
#: Placeholder for AWS compatible template listing capabilities
|
||||
#: required by the stack.
|
||||
capabilities = resource.Body('capabilities')
|
||||
|
@ -36,6 +39,9 @@ class Stack(resource.Resource):
|
|||
created_at = resource.Body('creation_time')
|
||||
#: A text description of the stack.
|
||||
description = resource.Body('description')
|
||||
#: A list of resource objects that will be deleted if a stack
|
||||
#: update is performed.
|
||||
deleted = resource.Body('deleted', type=list)
|
||||
#: Whether the stack will support a rollback operation on stack
|
||||
#: create/update failures. *Type: bool*
|
||||
is_rollback_disabled = resource.Body('disable_rollback', type=bool)
|
||||
|
@ -43,6 +49,7 @@ class Stack(resource.Resource):
|
|||
links = resource.Body('links')
|
||||
#: Name of the stack.
|
||||
name = resource.Body('stack_name')
|
||||
stack_name = resource.URI('stack_name')
|
||||
#: Placeholder for future extensions where stack related events
|
||||
#: can be published.
|
||||
notification_topics = resource.Body('notification_topics')
|
||||
|
@ -54,6 +61,9 @@ class Stack(resource.Resource):
|
|||
parameters = resource.Body('parameters', type=dict)
|
||||
#: The ID of the parent stack if any
|
||||
parent_id = resource.Body('parent')
|
||||
#: A list of resource objects that will be replaced if a stack update
|
||||
#: is performed.
|
||||
replaced = resource.Body('replaced')
|
||||
#: A string representation of the stack status, e.g. ``CREATE_COMPLETE``.
|
||||
status = resource.Body('stack_status')
|
||||
#: A text explaining how the stack transits to its current status.
|
||||
|
@ -69,6 +79,12 @@ class Stack(resource.Resource):
|
|||
template_url = resource.Body('template_url')
|
||||
#: Stack operation timeout in minutes.
|
||||
timeout_mins = resource.Body('timeout_mins')
|
||||
#: A list of resource objects that will remain unchanged if a stack
|
||||
#: update is performed.
|
||||
unchanged = resource.Body('unchanged')
|
||||
#: A list of resource objects that will have their properties updated
|
||||
#: in place if a stack update is performed.
|
||||
updated = resource.Body('updated')
|
||||
#: Timestamp of last update on the stack.
|
||||
updated_at = resource.Body('updated_time')
|
||||
#: The ID of the user project created for this stack.
|
||||
|
@ -86,6 +102,27 @@ class Stack(resource.Resource):
|
|||
return super(Stack, self).commit(session, prepend_key=False,
|
||||
has_body=False, base_path=None)
|
||||
|
||||
def update(self, session, preview=False):
|
||||
# This overrides the default behavior of resource update because
|
||||
# we need to use other endpoint for update preview.
|
||||
request = self._prepare_request(
|
||||
prepend_key=False,
|
||||
base_path='/stacks/%(stack_name)s/' % {'stack_name': self.name})
|
||||
|
||||
microversion = self._get_microversion_for(session, 'commit')
|
||||
|
||||
request_url = request.url
|
||||
if preview:
|
||||
request_url = utils.urljoin(request_url, 'preview')
|
||||
|
||||
response = session.put(
|
||||
request_url, json=request.body, headers=request.headers,
|
||||
microversion=microversion)
|
||||
|
||||
self.microversion = microversion
|
||||
self._translate_response(response, has_body=True)
|
||||
return self
|
||||
|
||||
def _action(self, session, body):
|
||||
"""Perform stack actions"""
|
||||
url = utils.urljoin(self.base_path, self._get_id(self), 'actions')
|
||||
|
@ -95,6 +132,12 @@ class Stack(resource.Resource):
|
|||
def check(self, session):
|
||||
return self._action(session, {'check': ''})
|
||||
|
||||
def abandon(self, session):
|
||||
url = utils.urljoin(self.base_path, self.name,
|
||||
self._get_id(self), 'abandon')
|
||||
resp = session.delete(url)
|
||||
return resp.json()
|
||||
|
||||
def fetch(self, session, requires_id=True,
|
||||
base_path=None, error_message=None):
|
||||
stk = super(Stack, self).fetch(
|
||||
|
@ -108,11 +151,4 @@ class Stack(resource.Resource):
|
|||
return stk
|
||||
|
||||
|
||||
class StackPreview(Stack):
|
||||
base_path = '/stacks/preview'
|
||||
|
||||
allow_create = True
|
||||
allow_list = False
|
||||
allow_fetch = False
|
||||
allow_commit = False
|
||||
allow_delete = False
|
||||
StackPreview = Stack
|
||||
|
|
|
@ -36,7 +36,7 @@ class TestOrchestrationProxy(test_proxy_base.TestProxyBase):
|
|||
|
||||
def test_create_stack_preview(self):
|
||||
method_kwargs = {"preview": True, "x": 1, "y": 2, "z": 3}
|
||||
self.verify_create(self.proxy.create_stack, stack.StackPreview,
|
||||
self.verify_create(self.proxy.create_stack, stack.Stack,
|
||||
method_kwargs=method_kwargs)
|
||||
|
||||
def test_find_stack(self):
|
||||
|
@ -52,7 +52,27 @@ class TestOrchestrationProxy(test_proxy_base.TestProxyBase):
|
|||
'openstack.orchestration.v1.stack.Stack')
|
||||
|
||||
def test_update_stack(self):
|
||||
self.verify_update(self.proxy.update_stack, stack.Stack)
|
||||
self._verify2('openstack.orchestration.v1.stack.Stack.update',
|
||||
self.proxy.update_stack,
|
||||
expected_result='result',
|
||||
method_args=['stack'],
|
||||
method_kwargs={'preview': False},
|
||||
expected_args=[self.proxy, False])
|
||||
|
||||
def test_update_stack_preview(self):
|
||||
self._verify2('openstack.orchestration.v1.stack.Stack.update',
|
||||
self.proxy.update_stack,
|
||||
expected_result='result',
|
||||
method_args=['stack'],
|
||||
method_kwargs={'preview': True},
|
||||
expected_args=[self.proxy, True])
|
||||
|
||||
def test_abandon_stack(self):
|
||||
self._verify2('openstack.orchestration.v1.stack.Stack.abandon',
|
||||
self.proxy.abandon_stack,
|
||||
expected_result='result',
|
||||
method_args=['stack'],
|
||||
expected_args=[self.proxy])
|
||||
|
||||
def test_delete_stack(self):
|
||||
self.verify_delete(self.proxy.delete_stack, stack.Stack, False)
|
||||
|
|
|
@ -49,6 +49,73 @@ FAKE_CREATE_RESPONSE = {
|
|||
'href': 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID),
|
||||
'rel': 'self'}]}
|
||||
}
|
||||
FAKE_UPDATE_PREVIEW_RESPONSE = {
|
||||
'unchanged': [
|
||||
{
|
||||
'updated_time': 'datetime',
|
||||
'resource_name': '',
|
||||
'physical_resource_id': '{resource id or ''}',
|
||||
'resource_action': 'CREATE',
|
||||
'resource_status': 'COMPLETE',
|
||||
'resource_status_reason': '',
|
||||
'resource_type': 'restype',
|
||||
'stack_identity': '{stack_id}',
|
||||
'stack_name': '{stack_name}'
|
||||
}
|
||||
],
|
||||
'updated': [
|
||||
{
|
||||
'updated_time': 'datetime',
|
||||
'resource_name': '',
|
||||
'physical_resource_id': '{resource id or ''}',
|
||||
'resource_action': 'CREATE',
|
||||
'resource_status': 'COMPLETE',
|
||||
'resource_status_reason': '',
|
||||
'resource_type': 'restype',
|
||||
'stack_identity': '{stack_id}',
|
||||
'stack_name': '{stack_name}'
|
||||
}
|
||||
],
|
||||
'replaced': [
|
||||
{
|
||||
'updated_time': 'datetime',
|
||||
'resource_name': '',
|
||||
'physical_resource_id': '{resource id or ''}',
|
||||
'resource_action': 'CREATE',
|
||||
'resource_status': 'COMPLETE',
|
||||
'resource_status_reason': '',
|
||||
'resource_type': 'restype',
|
||||
'stack_identity': '{stack_id}',
|
||||
'stack_name': '{stack_name}'
|
||||
}
|
||||
],
|
||||
'added': [
|
||||
{
|
||||
'updated_time': 'datetime',
|
||||
'resource_name': '',
|
||||
'physical_resource_id': '{resource id or ''}',
|
||||
'resource_action': 'CREATE',
|
||||
'resource_status': 'COMPLETE',
|
||||
'resource_status_reason': '',
|
||||
'resource_type': 'restype',
|
||||
'stack_identity': '{stack_id}',
|
||||
'stack_name': '{stack_name}'
|
||||
}
|
||||
],
|
||||
'deleted': [
|
||||
{
|
||||
'updated_time': 'datetime',
|
||||
'resource_name': '',
|
||||
'physical_resource_id': '{resource id or ''}',
|
||||
'resource_action': 'CREATE',
|
||||
'resource_status': 'COMPLETE',
|
||||
'resource_status_reason': '',
|
||||
'resource_type': 'restype',
|
||||
'stack_identity': '{stack_id}',
|
||||
'stack_name': '{stack_name}'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class TestStack(base.TestCase):
|
||||
|
@ -138,15 +205,78 @@ class TestStack(base.TestCase):
|
|||
self.assertEqual('No stack found for %s' % FAKE_ID,
|
||||
six.text_type(ex))
|
||||
|
||||
def test_abandon(self):
|
||||
sess = mock.Mock()
|
||||
sess.default_microversion = None
|
||||
|
||||
class TestStackPreview(base.TestCase):
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.headers = {}
|
||||
mock_response.json.return_value = {}
|
||||
sess.delete = mock.Mock(return_value=mock_response)
|
||||
sot = stack.Stack(**FAKE)
|
||||
|
||||
def test_basic(self):
|
||||
sot = stack.StackPreview()
|
||||
sot.abandon(sess)
|
||||
|
||||
self.assertEqual('/stacks/preview', sot.base_path)
|
||||
self.assertTrue(sot.allow_create)
|
||||
self.assertFalse(sot.allow_list)
|
||||
self.assertFalse(sot.allow_fetch)
|
||||
self.assertFalse(sot.allow_commit)
|
||||
self.assertFalse(sot.allow_delete)
|
||||
sess.delete.assert_called_with(
|
||||
'stacks/%s/%s/abandon' % (FAKE_NAME, FAKE_ID),
|
||||
|
||||
)
|
||||
|
||||
def test_update(self):
|
||||
sess = mock.Mock()
|
||||
sess.default_microversion = None
|
||||
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.headers = {}
|
||||
mock_response.json.return_value = {}
|
||||
sess.put = mock.Mock(return_value=mock_response)
|
||||
sot = stack.Stack(**FAKE)
|
||||
body = sot._body.dirty.copy()
|
||||
|
||||
sot.update(sess)
|
||||
|
||||
sess.put.assert_called_with(
|
||||
'stacks/%s/%s' % (FAKE_NAME, FAKE_ID),
|
||||
headers={},
|
||||
microversion=None,
|
||||
json=body
|
||||
)
|
||||
|
||||
def test_update_preview(self):
|
||||
sess = mock.Mock()
|
||||
sess.default_microversion = None
|
||||
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.headers = {}
|
||||
mock_response.json.return_value = FAKE_UPDATE_PREVIEW_RESPONSE.copy()
|
||||
sess.put = mock.Mock(return_value=mock_response)
|
||||
sot = stack.Stack(**FAKE)
|
||||
body = sot._body.dirty.copy()
|
||||
|
||||
ret = sot.update(sess, preview=True)
|
||||
|
||||
sess.put.assert_called_with(
|
||||
'stacks/%s/%s/preview' % (FAKE_NAME, FAKE_ID),
|
||||
headers={},
|
||||
microversion=None,
|
||||
json=body
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
FAKE_UPDATE_PREVIEW_RESPONSE['added'],
|
||||
ret.added)
|
||||
self.assertEqual(
|
||||
FAKE_UPDATE_PREVIEW_RESPONSE['deleted'],
|
||||
ret.deleted)
|
||||
self.assertEqual(
|
||||
FAKE_UPDATE_PREVIEW_RESPONSE['replaced'],
|
||||
ret.replaced)
|
||||
self.assertEqual(
|
||||
FAKE_UPDATE_PREVIEW_RESPONSE['unchanged'],
|
||||
ret.unchanged)
|
||||
self.assertEqual(
|
||||
FAKE_UPDATE_PREVIEW_RESPONSE['updated'],
|
||||
ret.updated)
|
||||
|
|
Loading…
Reference in New Issue