Coverage for nova/api/openstack/compute/aggregates.py: 98%
213 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
1# Copyright (c) 2012 Citrix Systems, Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
16"""The Aggregate admin API extension."""
18import datetime
20from oslo_log import log as logging
21from webob import exc
23from nova.api.openstack import api_version_request
24from nova.api.openstack import common
25from nova.api.openstack.compute.schemas import aggregate_images
26from nova.api.openstack.compute.schemas import aggregates as schema
27from nova.api.openstack import wsgi
28from nova.api import validation
29from nova.compute import api as compute
30from nova.conductor import api as conductor
31from nova import exception
32from nova.i18n import _
33from nova.policies import aggregates as aggr_policies
34from nova import utils
36LOG = logging.getLogger(__name__)
39def _get_context(req):
40 return req.environ['nova.context']
43class AggregateController(wsgi.Controller):
44 """The Host Aggregates API controller for the OpenStack API."""
46 def __init__(self):
47 super(AggregateController, self).__init__()
48 self.api = compute.AggregateAPI()
49 self.conductor_tasks = conductor.ComputeTaskAPI()
51 @wsgi.expected_errors(())
52 @validation.query_schema(schema.index_query)
53 @validation.response_body_schema(schema.index_response, '2.1', '2.40')
54 @validation.response_body_schema(schema.index_response_v241, '2.41')
55 def index(self, req):
56 """Returns a list a host aggregate's id, name, availability_zone."""
57 context = _get_context(req)
58 context.can(aggr_policies.POLICY_ROOT % 'index', target={})
59 aggregates = self.api.get_aggregate_list(context)
60 return {'aggregates': [self._marshall_aggregate(req, a)['aggregate']
61 for a in aggregates]}
63 # NOTE(gmann): Returns 200 for backwards compatibility but should be 201
64 # as this operation complete the creation of aggregates resource.
65 @wsgi.expected_errors((400, 409))
66 @validation.schema(schema.create_v20, '2.0', '2.0')
67 @validation.schema(schema.create, '2.1')
68 @validation.response_body_schema(schema.create_response, '2.1', '2.40')
69 @validation.response_body_schema(schema.create_response_v241, '2.41')
70 def create(self, req, body):
71 """Creates an aggregate, given its name and
72 optional availability zone.
73 """
74 context = _get_context(req)
75 context.can(aggr_policies.POLICY_ROOT % 'create', target={})
76 host_aggregate = body["aggregate"]
77 name = common.normalize_name(host_aggregate["name"])
78 avail_zone = host_aggregate.get("availability_zone")
79 if avail_zone:
80 avail_zone = common.normalize_name(avail_zone)
82 try:
83 aggregate = self.api.create_aggregate(context, name, avail_zone)
84 except exception.AggregateNameExists as e:
85 raise exc.HTTPConflict(explanation=e.format_message())
86 except exception.ObjectActionError:
87 raise exc.HTTPConflict(explanation=_(
88 'Not all aggregates have been migrated to the API database'))
89 except exception.InvalidAggregateAction as e:
90 raise exc.HTTPBadRequest(explanation=e.format_message())
92 agg = self._marshall_aggregate(req, aggregate)
94 # To maintain the same API result as before the changes for returning
95 # nova objects were made.
96 del agg['aggregate']['hosts']
97 del agg['aggregate']['metadata']
99 return agg
101 @wsgi.expected_errors((400, 404))
102 @validation.query_schema(schema.show_query)
103 @validation.response_body_schema(schema.show_response, '2.1', '2.40')
104 @validation.response_body_schema(schema.show_response_v241, '2.41')
105 def show(self, req, id):
106 """Shows the details of an aggregate, hosts and metadata included."""
107 context = _get_context(req)
108 context.can(aggr_policies.POLICY_ROOT % 'show', target={})
110 try:
111 utils.validate_integer(id, 'id')
112 except exception.InvalidInput as e:
113 raise exc.HTTPBadRequest(explanation=e.format_message())
115 try:
116 aggregate = self.api.get_aggregate(context, id)
117 except exception.AggregateNotFound as e:
118 raise exc.HTTPNotFound(explanation=e.format_message())
119 return self._marshall_aggregate(req, aggregate)
121 @wsgi.expected_errors((400, 404, 409))
122 @validation.schema(schema.update_v20, '2.0', '2.0')
123 @validation.schema(schema.update, '2.1')
124 @validation.response_body_schema(schema.update_response, '2.1', '2.40')
125 @validation.response_body_schema(schema.update_response_v241, '2.41')
126 def update(self, req, id, body):
127 """Updates the name and/or availability_zone of given aggregate."""
128 context = _get_context(req)
129 context.can(aggr_policies.POLICY_ROOT % 'update', target={})
130 updates = body["aggregate"]
131 if 'name' in updates:
132 updates['name'] = common.normalize_name(updates['name'])
134 try:
135 utils.validate_integer(id, 'id')
136 except exception.InvalidInput as e:
137 raise exc.HTTPBadRequest(explanation=e.format_message())
139 try:
140 aggregate = self.api.update_aggregate(context, id, updates)
141 except exception.AggregateNameExists as e:
142 raise exc.HTTPConflict(explanation=e.format_message())
143 except exception.AggregateNotFound as e:
144 raise exc.HTTPNotFound(explanation=e.format_message())
145 except exception.InvalidAggregateAction as e:
146 raise exc.HTTPBadRequest(explanation=e.format_message())
148 return self._marshall_aggregate(req, aggregate)
150 # NOTE(gmann): Returns 200 for backwards compatibility but should be 204
151 # as this operation complete the deletion of aggregate resource and return
152 # no response body.
153 @wsgi.expected_errors((400, 404))
154 @validation.response_body_schema(schema.delete_response)
155 def delete(self, req, id):
156 """Removes an aggregate by id."""
157 context = _get_context(req)
158 context.can(aggr_policies.POLICY_ROOT % 'delete', target={})
160 try:
161 utils.validate_integer(id, 'id')
162 except exception.InvalidInput as e:
163 raise exc.HTTPBadRequest(explanation=e.format_message())
165 try:
166 self.api.delete_aggregate(context, id)
167 except exception.AggregateNotFound as e:
168 raise exc.HTTPNotFound(explanation=e.format_message())
169 except exception.InvalidAggregateAction as e:
170 raise exc.HTTPBadRequest(explanation=e.format_message())
172 # NOTE(gmann): Returns 200 for backwards compatibility but should be 202
173 # for representing async API as this API just accepts the request and
174 # request hypervisor driver to complete the same in async mode.
175 @wsgi.expected_errors((400, 404, 409))
176 @wsgi.action('add_host')
177 @validation.schema(schema.add_host)
178 @validation.response_body_schema(schema.add_host_response, '2.1', '2.40') # noqa: E501
179 @validation.response_body_schema(schema.add_host_response_v241, '2.41')
180 def _add_host(self, req, id, body):
181 """Adds a host to the specified aggregate."""
182 host = body['add_host']['host']
184 context = _get_context(req)
185 context.can(aggr_policies.POLICY_ROOT % 'add_host', target={})
187 try:
188 utils.validate_integer(id, 'id')
189 except exception.InvalidInput as e:
190 raise exc.HTTPBadRequest(explanation=e.format_message())
192 try:
193 aggregate = self.api.add_host_to_aggregate(context, id, host)
194 except (exception.AggregateNotFound,
195 exception.HostMappingNotFound,
196 exception.ComputeHostNotFound) as e:
197 raise exc.HTTPNotFound(explanation=e.format_message())
198 except (exception.AggregateHostExists,
199 exception.InvalidAggregateAction) as e:
200 raise exc.HTTPConflict(explanation=e.format_message())
201 return self._marshall_aggregate(req, aggregate)
203 # NOTE(gmann): Returns 200 for backwards compatibility but should be 202
204 # for representing async API as this API just accepts the request and
205 # request hypervisor driver to complete the same in async mode.
206 @wsgi.expected_errors((400, 404, 409))
207 @wsgi.action('remove_host')
208 @validation.schema(schema.remove_host)
209 @validation.response_body_schema(schema.remove_host_response, '2.1', '2.40') # noqa: E501
210 @validation.response_body_schema(schema.remove_host_response_v241, '2.41')
211 def _remove_host(self, req, id, body):
212 """Removes a host from the specified aggregate."""
213 host = body['remove_host']['host']
215 context = _get_context(req)
216 context.can(aggr_policies.POLICY_ROOT % 'remove_host', target={})
218 try:
219 utils.validate_integer(id, 'id')
220 except exception.InvalidInput as e:
221 raise exc.HTTPBadRequest(explanation=e.format_message())
223 try:
224 aggregate = self.api.remove_host_from_aggregate(context, id, host)
225 except (exception.AggregateNotFound,
226 exception.AggregateHostNotFound,
227 exception.ComputeHostNotFound) as e:
228 LOG.error('Failed to remove host %s from aggregate %s. Error: %s',
229 host, id, str(e))
230 msg = _('Cannot remove host %(host)s in aggregate %(id)s') % {
231 'host': host, 'id': id}
232 raise exc.HTTPNotFound(explanation=msg)
233 except (exception.InvalidAggregateAction,
234 exception.ResourceProviderUpdateConflict) as e:
235 LOG.error('Failed to remove host %s from aggregate %s. Error: %s',
236 host, id, str(e))
237 raise exc.HTTPConflict(explanation=e.format_message())
238 return self._marshall_aggregate(req, aggregate)
240 @wsgi.expected_errors((400, 404))
241 @wsgi.action('set_metadata')
242 @validation.schema(schema.set_metadata)
243 @validation.response_body_schema(schema.set_metadata_response, '2.1', '2.40') # noqa: E501
244 @validation.response_body_schema(schema.set_metadata_response_v241, '2.41')
245 def _set_metadata(self, req, id, body):
246 """Replaces the aggregate's existing metadata with new metadata."""
247 context = _get_context(req)
248 context.can(aggr_policies.POLICY_ROOT % 'set_metadata', target={})
250 try:
251 utils.validate_integer(id, 'id')
252 except exception.InvalidInput as e:
253 raise exc.HTTPBadRequest(explanation=e.format_message())
255 metadata = body["set_metadata"]["metadata"]
256 try:
257 aggregate = self.api.update_aggregate_metadata(context,
258 id, metadata)
259 except exception.AggregateNotFound as e:
260 raise exc.HTTPNotFound(explanation=e.format_message())
261 except (exception.InvalidAggregateAction,
262 exception.AggregateMetadataKeyExists) as e:
263 raise exc.HTTPBadRequest(explanation=e.format_message())
265 return self._marshall_aggregate(req, aggregate)
267 def _marshall_aggregate(self, req, aggregate):
268 _aggregate = {}
269 for key, value in self._build_aggregate_items(req, aggregate):
270 # NOTE(danms): The original API specified non-TZ-aware timestamps
271 if isinstance(value, datetime.datetime):
272 value = value.replace(tzinfo=None)
273 _aggregate[key] = value
274 return {"aggregate": _aggregate}
276 def _build_aggregate_items(self, req, aggregate):
277 show_uuid = api_version_request.is_supported(req, min_version="2.41")
278 keys = aggregate.obj_fields
279 # NOTE(rlrossit): Within the compute API, metadata will always be
280 # set on the aggregate object (at a minimum to {}). Because of this,
281 # we can freely use getattr() on keys in obj_extra_fields (in this
282 # case it is only ['availability_zone']) without worrying about
283 # lazy-loading an unset variable
284 for key in keys:
285 if ((aggregate.obj_attr_is_set(key) or
286 key in aggregate.obj_extra_fields) and
287 (show_uuid or key != 'uuid')):
288 yield key, getattr(aggregate, key)
290 @wsgi.Controller.api_version('2.81')
291 @wsgi.response(202)
292 @wsgi.expected_errors((400, 404))
293 @validation.schema(aggregate_images.aggregate_images)
294 @validation.response_body_schema(
295 aggregate_images.aggregate_images_response)
296 def images(self, req, id, body):
297 """Allows image cache management requests."""
298 context = _get_context(req)
299 context.can(aggr_policies.NEW_POLICY_ROOT % 'images', target={})
301 try:
302 utils.validate_integer(id, 'id')
303 except exception.InvalidInput as e:
304 raise exc.HTTPBadRequest(explanation=e.format_message())
306 image_ids = []
307 for image_req in body.get('cache'):
308 image_ids.append(image_req['id'])
310 if sorted(image_ids) != sorted(list(set(image_ids))):
311 raise exc.HTTPBadRequest(
312 explanation=_('Duplicate images in request'))
314 try:
315 aggregate = self.api.get_aggregate(context, id)
316 except exception.AggregateNotFound as e:
317 raise exc.HTTPNotFound(explanation=e.format_message())
319 try:
320 self.conductor_tasks.cache_images(context, aggregate, image_ids)
321 except exception.NovaException as e:
322 raise exc.HTTPBadRequest(explanation=e.format_message())