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:
Artem Goncharov 2018-12-04 15:50:27 +01:00
parent 8274409c9e
commit aea3ea2ad1
4 changed files with 222 additions and 27 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)