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

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. 

15 

16"""The Aggregate admin API extension.""" 

17 

18import datetime 

19 

20from oslo_log import log as logging 

21from webob import exc 

22 

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 

35 

36LOG = logging.getLogger(__name__) 

37 

38 

39def _get_context(req): 

40 return req.environ['nova.context'] 

41 

42 

43class AggregateController(wsgi.Controller): 

44 """The Host Aggregates API controller for the OpenStack API.""" 

45 

46 def __init__(self): 

47 super(AggregateController, self).__init__() 

48 self.api = compute.AggregateAPI() 

49 self.conductor_tasks = conductor.ComputeTaskAPI() 

50 

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]} 

62 

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) 

81 

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

91 

92 agg = self._marshall_aggregate(req, aggregate) 

93 

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'] 

98 

99 return agg 

100 

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={}) 

109 

110 try: 

111 utils.validate_integer(id, 'id') 

112 except exception.InvalidInput as e: 

113 raise exc.HTTPBadRequest(explanation=e.format_message()) 

114 

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) 

120 

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']) 

133 

134 try: 

135 utils.validate_integer(id, 'id') 

136 except exception.InvalidInput as e: 

137 raise exc.HTTPBadRequest(explanation=e.format_message()) 

138 

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

147 

148 return self._marshall_aggregate(req, aggregate) 

149 

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={}) 

159 

160 try: 

161 utils.validate_integer(id, 'id') 

162 except exception.InvalidInput as e: 

163 raise exc.HTTPBadRequest(explanation=e.format_message()) 

164 

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

171 

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'] 

183 

184 context = _get_context(req) 

185 context.can(aggr_policies.POLICY_ROOT % 'add_host', target={}) 

186 

187 try: 

188 utils.validate_integer(id, 'id') 

189 except exception.InvalidInput as e: 

190 raise exc.HTTPBadRequest(explanation=e.format_message()) 

191 

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) 

202 

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'] 

214 

215 context = _get_context(req) 

216 context.can(aggr_policies.POLICY_ROOT % 'remove_host', target={}) 

217 

218 try: 

219 utils.validate_integer(id, 'id') 

220 except exception.InvalidInput as e: 

221 raise exc.HTTPBadRequest(explanation=e.format_message()) 

222 

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) 

239 

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={}) 

249 

250 try: 

251 utils.validate_integer(id, 'id') 

252 except exception.InvalidInput as e: 

253 raise exc.HTTPBadRequest(explanation=e.format_message()) 

254 

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

264 

265 return self._marshall_aggregate(req, aggregate) 

266 

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} 

275 

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) 

289 

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={}) 

300 

301 try: 

302 utils.validate_integer(id, 'id') 

303 except exception.InvalidInput as e: 

304 raise exc.HTTPBadRequest(explanation=e.format_message()) 

305 

306 image_ids = [] 

307 for image_req in body.get('cache'): 

308 image_ids.append(image_req['id']) 

309 

310 if sorted(image_ids) != sorted(list(set(image_ids))): 

311 raise exc.HTTPBadRequest( 

312 explanation=_('Duplicate images in request')) 

313 

314 try: 

315 aggregate = self.api.get_aggregate(context, id) 

316 except exception.AggregateNotFound as e: 

317 raise exc.HTTPNotFound(explanation=e.format_message()) 

318 

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