1# Licensed under the Apache License, Version 2.0 (the "License"); you may 2# not use this file except in compliance with the License. You may obtain 3# a copy of the License at 4# 5# http://www.apache.org/licenses/LICENSE-2.0 6# 7# Unless required by applicable law or agreed to in writing, software 8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10# License for the specific language governing permissions and limitations 11# under the License. 12 13from openstack import exceptions 14from openstack import resource 15from openstack import utils 16 17 18class Stack(resource.Resource): 19 name_attribute = 'stack_name' 20 resource_key = 'stack' 21 resources_key = 'stacks' 22 base_path = '/stacks' 23 24 # capabilities 25 allow_create = True 26 allow_list = True 27 allow_fetch = True 28 allow_commit = True 29 allow_delete = True 30 31 _query_mapping = resource.QueryParameters( 32 'action', 'name', 'status', 33 'project_id', 'owner_id', 'username', 34 project_id='tenant_id', 35 **resource.TagMixin._tag_query_parameters 36 ) 37 38 # Properties 39 #: A list of resource objects that will be added if a stack update 40 # is performed. 41 added = resource.Body('added') 42 #: Placeholder for AWS compatible template listing capabilities 43 #: required by the stack. 44 capabilities = resource.Body('capabilities') 45 #: Timestamp of the stack creation. 46 created_at = resource.Body('creation_time') 47 #: A text description of the stack. 48 description = resource.Body('description') 49 #: A list of resource objects that will be deleted if a stack 50 #: update is performed. 51 deleted = resource.Body('deleted', type=list) 52 #: Timestamp of the stack deletion. 53 deleted_at = resource.Body('deletion_time') 54 #: A JSON environment for the stack. 55 environment = resource.Body('environment') 56 #: An ordered list of names for environment files found in the files dict. 57 environment_files = resource.Body('environment_files', type=list) 58 #: Additional files referenced in the template or the environment 59 files = resource.Body('files', type=dict) 60 #: Name of the container in swift that has child 61 #: templates and environment files. 62 files_container = resource.Body('files_container') 63 #: Whether the stack will support a rollback operation on stack 64 #: create/update failures. *Type: bool* 65 is_rollback_disabled = resource.Body('disable_rollback', type=bool) 66 #: A list of dictionaries containing links relevant to the stack. 67 links = resource.Body('links') 68 #: Name of the stack. 69 name = resource.Body('stack_name') 70 stack_name = resource.URI('stack_name') 71 #: Placeholder for future extensions where stack related events 72 #: can be published. 73 notification_topics = resource.Body('notification_topics') 74 #: A list containing output keys and values from the stack, if any. 75 outputs = resource.Body('outputs') 76 #: The ID of the owner stack if any. 77 owner_id = resource.Body('stack_owner') 78 #: A dictionary containing the parameter names and values for the stack. 79 parameters = resource.Body('parameters', type=dict) 80 #: The ID of the parent stack if any 81 parent_id = resource.Body('parent') 82 #: A list of resource objects that will be replaced if a stack update 83 #: is performed. 84 replaced = resource.Body('replaced') 85 #: A string representation of the stack status, e.g. ``CREATE_COMPLETE``. 86 status = resource.Body('stack_status') 87 #: A text explaining how the stack transits to its current status. 88 status_reason = resource.Body('stack_status_reason') 89 #: A list of strings used as tags on the stack 90 tags = resource.Body('tags', type=list, default=[]) 91 #: A dict containing the template use for stack creation. 92 template = resource.Body('template', type=dict) 93 #: Stack template description text. Currently contains the same text 94 #: as that of the ``description`` property. 95 template_description = resource.Body('template_description') 96 #: A string containing the URL where a stack template can be found. 97 template_url = resource.Body('template_url') 98 #: Stack operation timeout in minutes. 99 timeout_mins = resource.Body('timeout_mins') 100 #: A list of resource objects that will remain unchanged if a stack 101 #: update is performed. 102 unchanged = resource.Body('unchanged') 103 #: A list of resource objects that will have their properties updated 104 #: in place if a stack update is performed. 105 updated = resource.Body('updated') 106 #: Timestamp of last update on the stack. 107 updated_at = resource.Body('updated_time') 108 #: The ID of the user project created for this stack. 109 user_project_id = resource.Body('stack_user_project_id') 110 111 def create(self, session, base_path=None): 112 # This overrides the default behavior of resource creation because 113 # heat doesn't accept resource_key in its request. 114 return super(Stack, self).create(session, prepend_key=False, 115 base_path=base_path) 116 117 def commit(self, session, base_path=None): 118 # This overrides the default behavior of resource creation because 119 # heat doesn't accept resource_key in its request. 120 return super(Stack, self).commit(session, prepend_key=False, 121 has_body=False, base_path=None) 122 123 def update(self, session, preview=False): 124 # This overrides the default behavior of resource update because 125 # we need to use other endpoint for update preview. 126 base_path = None 127 if self.name and self.id: 128 base_path = '/stacks/%(stack_name)s/%(stack_id)s' % { 129 'stack_name': self.name, 130 'stack_id': self.id} 131 elif self.name or self.id: 132 # We have only one of name/id. Do not try to build a stacks/NAME/ID 133 # path 134 base_path = '/stacks/%(stack_identity)s' % { 135 'stack_identity': self.name or self.id} 136 request = self._prepare_request( 137 prepend_key=False, 138 requires_id=False, 139 base_path=base_path) 140 141 microversion = self._get_microversion_for(session, 'commit') 142 143 request_url = request.url 144 if preview: 145 request_url = utils.urljoin(request_url, 'preview') 146 147 response = session.put( 148 request_url, json=request.body, headers=request.headers, 149 microversion=microversion) 150 151 self.microversion = microversion 152 self._translate_response(response, has_body=True) 153 return self 154 155 def _action(self, session, body): 156 """Perform stack actions""" 157 url = utils.urljoin(self.base_path, self._get_id(self), 'actions') 158 resp = session.post(url, json=body) 159 return resp.json() 160 161 def check(self, session): 162 return self._action(session, {'check': ''}) 163 164 def abandon(self, session): 165 url = utils.urljoin(self.base_path, self.name, 166 self._get_id(self), 'abandon') 167 resp = session.delete(url) 168 return resp.json() 169 170 def fetch(self, session, requires_id=True, 171 base_path=None, error_message=None, resolve_outputs=True): 172 173 if not self.allow_fetch: 174 raise exceptions.MethodNotSupported(self, "fetch") 175 176 request = self._prepare_request(requires_id=requires_id, 177 base_path=base_path) 178 # session = self._get_session(session) 179 microversion = self._get_microversion_for(session, 'fetch') 180 181 # NOTE(gtema): would be nice to simply use QueryParameters, however 182 # Heat return 302 with parameters being set into URL and requests 183 # apply parameters again, what results in them being set doubled 184 if not resolve_outputs: 185 request.url = request.url + '?resolve_outputs=False' 186 response = session.get(request.url, microversion=microversion) 187 kwargs = {} 188 if error_message: 189 kwargs['error_message'] = error_message 190 191 self.microversion = microversion 192 self._translate_response(response, **kwargs) 193 194 if self and self.status in ['DELETE_COMPLETE', 'ADOPT_COMPLETE']: 195 raise exceptions.ResourceNotFound( 196 "No stack found for %s" % self.id) 197 return self 198 199 @classmethod 200 def find(cls, session, name_or_id, ignore_missing=True, **params): 201 """Find a resource by its name or id. 202 203 :param session: The session to use for making this request. 204 :type session: :class:`~keystoneauth1.adapter.Adapter` 205 :param name_or_id: This resource's identifier, if needed by 206 the request. The default is ``None``. 207 :param bool ignore_missing: When set to ``False`` 208 :class:`~openstack.exceptions.ResourceNotFound` will be 209 raised when the resource does not exist. 210 When set to ``True``, None will be returned when 211 attempting to find a nonexistent resource. 212 :param dict params: Any additional parameters to be passed into 213 underlying methods, such as to 214 :meth:`~openstack.resource.Resource.existing` 215 in order to pass on URI parameters. 216 217 :return: The :class:`Resource` object matching the given name or id 218 or None if nothing matches. 219 :raises: :class:`openstack.exceptions.DuplicateResource` if more 220 than one resource is found for this request. 221 :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing 222 is found and ignore_missing is ``False``. 223 """ 224 session = cls._get_session(session) 225 # Try to short-circuit by looking directly for a matching ID. 226 try: 227 match = cls.existing( 228 id=name_or_id, 229 connection=session._get_connection(), 230 **params) 231 return match.fetch(session, **params) 232 except exceptions.NotFoundException: 233 pass 234 235 # NOTE(gtema) we do not do list, since previous call has done this 236 # for us already 237 238 if ignore_missing: 239 return None 240 raise exceptions.ResourceNotFound( 241 "No %s found for %s" % (cls.__name__, name_or_id)) 242 243 244StackPreview = Stack 245