From aea3ea2ad1fe16831997e6a360ad55b53884bb14 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 4 Dec 2018 15:50:27 +0100 Subject: [PATCH] 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 --- openstack/orchestration/v1/_proxy.py | 25 ++- openstack/orchestration/v1/stack.py | 52 +++++- .../tests/unit/orchestration/v1/test_proxy.py | 24 ++- .../tests/unit/orchestration/v1/test_stack.py | 148 ++++++++++++++++-- 4 files changed, 222 insertions(+), 27 deletions(-) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 7b0423621..237f78bce 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -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 diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index c2e464293..8f31dbee8 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -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 diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 0fc39487b..c6cc44359 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -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) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 967265ce0..698d877a7 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -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)