Coverage for nova/context.py: 85%

203 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-24 11:16 +0000

1# Copyright 2011 OpenStack Foundation 

2# Copyright 2010 United States Government as represented by the 

3# Administrator of the National Aeronautics and Space Administration. 

4# All Rights Reserved. 

5# 

6# Licensed under the Apache License, Version 2.0 (the "License"); you may 

7# not use this file except in compliance with the License. You may obtain 

8# a copy of the License at 

9# 

10# http://www.apache.org/licenses/LICENSE-2.0 

11# 

12# Unless required by applicable law or agreed to in writing, software 

13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

15# License for the specific language governing permissions and limitations 

16# under the License. 

17 

18"""RequestContext: context for requests that persist through all of nova.""" 

19 

20from contextlib import contextmanager 

21import copy 

22 

23import eventlet.queue 

24import eventlet.timeout 

25from keystoneauth1.access import service_catalog as ksa_service_catalog 

26from keystoneauth1 import plugin 

27from oslo_context import context 

28from oslo_db.sqlalchemy import enginefacade 

29from oslo_log import log as logging 

30from oslo_utils import timeutils 

31 

32from nova import exception 

33from nova.i18n import _ 

34from nova import objects 

35from nova import policy 

36from nova import utils 

37 

38LOG = logging.getLogger(__name__) 

39CELL_CACHE = {} 

40# NOTE(melwitt): Used for the scatter-gather utility to indicate we timed out 

41# waiting for a result from a cell. 

42did_not_respond_sentinel = object() 

43# FIXME(danms): Keep a global cache of the cells we find the 

44# first time we look. This needs to be refreshed on a timer or 

45# trigger. 

46CELLS = [] 

47# Timeout value for waiting for cells to respond 

48CELL_TIMEOUT = 60 

49 

50 

51class _ContextAuthPlugin(plugin.BaseAuthPlugin): 

52 """A keystoneauth auth plugin that uses the values from the Context. 

53 

54 Ideally we would use the plugin provided by auth_token middleware however 

55 this plugin isn't serialized yet so we construct one from the serialized 

56 auth data. 

57 """ 

58 

59 def __init__(self, auth_token, sc): 

60 super(_ContextAuthPlugin, self).__init__() 

61 

62 self.auth_token = auth_token 

63 self.service_catalog = ksa_service_catalog.ServiceCatalogV2(sc) 

64 

65 def get_token(self, *args, **kwargs): 

66 return self.auth_token 

67 

68 def get_endpoint(self, session, service_type=None, interface=None, 

69 region_name=None, service_name=None, **kwargs): 

70 return self.service_catalog.url_for(service_type=service_type, 

71 service_name=service_name, 

72 interface=interface, 

73 region_name=region_name) 

74 

75 

76@enginefacade.transaction_context_provider 

77class RequestContext(context.RequestContext): 

78 """Security context and request information. 

79 

80 Represents the user taking a given action within the system. 

81 

82 """ 

83 

84 def __init__(self, user_id=None, project_id=None, is_admin=None, 

85 read_deleted="no", remote_address=None, timestamp=None, 

86 quota_class=None, service_catalog=None, 

87 user_auth_plugin=None, **kwargs): 

88 """:param read_deleted: 'no' indicates deleted records are hidden, 

89 'yes' indicates deleted records are visible, 

90 'only' indicates that *only* deleted records are visible. 

91 

92 :param overwrite: Set to False to ensure that the greenthread local 

93 copy of the index is not overwritten. 

94 

95 :param user_auth_plugin: The auth plugin for the current request's 

96 authentication data. 

97 """ 

98 if user_id: 

99 kwargs['user_id'] = user_id 

100 if project_id: 

101 kwargs['project_id'] = project_id 

102 

103 super(RequestContext, self).__init__(is_admin=is_admin, **kwargs) 

104 

105 self.read_deleted = read_deleted 

106 self.remote_address = remote_address 

107 if not timestamp: 

108 timestamp = timeutils.utcnow() 

109 if isinstance(timestamp, str): 

110 timestamp = timeutils.parse_strtime(timestamp) 

111 self.timestamp = timestamp 

112 

113 if service_catalog: 

114 # Only include required parts of service_catalog 

115 self.service_catalog = [s for s in service_catalog 

116 if s.get('type') in ('image', 'block-storage', 'volumev3', 

117 'key-manager', 'placement', 'network', 

118 'accelerator', 'sharev2')] 

119 else: 

120 # if list is empty or none 

121 self.service_catalog = [] 

122 

123 # NOTE(markmc): this attribute is currently only used by the 

124 # rs_limits turnstile pre-processor. 

125 # See https://lists.launchpad.net/openstack/msg12200.html 

126 self.quota_class = quota_class 

127 

128 # NOTE(dheeraj): The following attributes are used by cellsv2 to store 

129 # connection information for connecting to the target cell. 

130 # It is only manipulated using the target_cell contextmanager 

131 # provided by this module 

132 self.db_connection = None 

133 self.mq_connection = None 

134 self.cell_uuid = None 

135 

136 self.user_auth_plugin = user_auth_plugin 

137 if self.is_admin is None: 

138 self.is_admin = policy.check_is_admin(self) 

139 

140 def get_auth_plugin(self): 

141 if self.user_auth_plugin: 141 ↛ 142line 141 didn't jump to line 142 because the condition on line 141 was never true

142 return self.user_auth_plugin 

143 else: 

144 return _ContextAuthPlugin(self.auth_token, self.service_catalog) 

145 

146 def _get_read_deleted(self): 

147 return self._read_deleted 

148 

149 def _set_read_deleted(self, read_deleted): 

150 if read_deleted not in ('no', 'yes', 'only'): 

151 raise ValueError(_("read_deleted can only be one of 'no', " 

152 "'yes' or 'only', not %r") % read_deleted) 

153 self._read_deleted = read_deleted 

154 

155 def _del_read_deleted(self): 

156 del self._read_deleted 

157 

158 read_deleted = property(_get_read_deleted, _set_read_deleted, 

159 _del_read_deleted) 

160 

161 def to_dict(self): 

162 values = super(RequestContext, self).to_dict() 

163 # FIXME(dims): defensive hasattr() checks need to be 

164 # removed once we figure out why we are seeing stack 

165 # traces 

166 values.update({ 

167 'user_id': getattr(self, 'user_id', None), 

168 'project_id': getattr(self, 'project_id', None), 

169 'is_admin': getattr(self, 'is_admin', None), 

170 'read_deleted': getattr(self, 'read_deleted', 'no'), 

171 'remote_address': getattr(self, 'remote_address', None), 

172 'timestamp': utils.strtime(self.timestamp) if hasattr( 

173 self, 'timestamp') else None, 

174 'request_id': getattr(self, 'request_id', None), 

175 'quota_class': getattr(self, 'quota_class', None), 

176 'user_name': getattr(self, 'user_name', None), 

177 'service_catalog': getattr(self, 'service_catalog', None), 

178 'project_name': getattr(self, 'project_name', None), 

179 }) 

180 # NOTE(tonyb): This can be removed once we're certain to have a 

181 # RequestContext contains 'is_admin_project', We can only get away with 

182 # this because we "know" the default value of 'is_admin_project' which 

183 # is very fragile. 

184 values.update({ 

185 'is_admin_project': getattr(self, 'is_admin_project', True), 

186 }) 

187 return values 

188 

189 @classmethod 

190 def from_dict(cls, values): 

191 return super(RequestContext, cls).from_dict( 

192 values, 

193 user_id=values.get('user_id'), 

194 project_id=values.get('project_id'), 

195 # TODO(sdague): oslo.context has show_deleted, if 

196 # possible, we should migrate to that in the future so we 

197 # don't need to be different here. 

198 read_deleted=values.get('read_deleted', 'no'), 

199 remote_address=values.get('remote_address'), 

200 timestamp=values.get('timestamp'), 

201 quota_class=values.get('quota_class'), 

202 service_catalog=values.get('service_catalog'), 

203 ) 

204 

205 def elevated(self, read_deleted=None): 

206 """Return a version of this context with admin flag set.""" 

207 context = copy.copy(self) 

208 # context.roles must be deepcopied to leave original roles 

209 # without changes 

210 context.roles = copy.deepcopy(self.roles) 

211 context.is_admin = True 

212 

213 if 'admin' not in context.roles: 

214 context.roles.append('admin') 

215 

216 if read_deleted is not None: 

217 context.read_deleted = read_deleted 

218 

219 return context 

220 

221 def can(self, action, target=None, fatal=True): 

222 """Verifies that the given action is valid on the target in this 

223 context. 

224 

225 :param action: string representing the action to be checked. 

226 :param target: dictionary representing the object of the action 

227 for object creation this should be a dictionary representing the 

228 location of the object 

229 e.g. ``{'project_id': instance.project_id}``. 

230 :param fatal: if False, will return False when an exception.Forbidden 

231 occurs. 

232 

233 :raises nova.exception.Forbidden: if verification fails and fatal is 

234 True. 

235 

236 :return: returns a non-False value (not necessarily "True") if 

237 authorized and False if not authorized and fatal is False. 

238 """ 

239 try: 

240 return policy.authorize(self, action, target) 

241 except exception.Forbidden: 

242 if fatal: 

243 raise 

244 return False 

245 

246 def to_policy_values(self): 

247 policy = super(RequestContext, self).to_policy_values() 

248 policy['is_admin'] = self.is_admin 

249 return policy 

250 

251 def __str__(self): 

252 return "<Context %s>" % self.to_dict() 

253 

254 

255def get_context(): 

256 """A helper method to get a blank context. 

257 

258 Note that overwrite is False here so this context will not update the 

259 greenthread-local stored context that is used when logging. 

260 """ 

261 return RequestContext(user_id=None, 

262 project_id=None, 

263 is_admin=False, 

264 overwrite=False) 

265 

266 

267def get_admin_context(read_deleted="no"): 

268 # NOTE(alaski): This method should only be used when an admin context is 

269 # necessary for the entirety of the context lifetime. If that's not the 

270 # case please use get_context(), or create the RequestContext manually, and 

271 # use context.elevated() where necessary. Some periodic tasks may use 

272 # get_admin_context so that their database calls are not filtered on 

273 # project_id. 

274 return RequestContext(user_id=None, 

275 project_id=None, 

276 is_admin=True, 

277 read_deleted=read_deleted, 

278 overwrite=False) 

279 

280 

281def is_user_context(context): 

282 """Indicates if the request context is a normal user.""" 

283 if not context: 283 ↛ 284line 283 didn't jump to line 284 because the condition on line 283 was never true

284 return False 

285 if context.is_admin: 

286 return False 

287 if not context.user_id or not context.project_id: 

288 return False 

289 return True 

290 

291 

292def require_context(ctxt): 

293 """Raise exception.Forbidden() if context is not a user or an 

294 admin context. 

295 """ 

296 if not ctxt.is_admin and not is_user_context(ctxt): 296 ↛ 297line 296 didn't jump to line 297 because the condition on line 296 was never true

297 raise exception.Forbidden() 

298 

299 

300def authorize_project_context(context, project_id): 

301 """Ensures a request has permission to access the given project.""" 

302 if is_user_context(context): 

303 if not context.project_id: 

304 raise exception.Forbidden() 

305 elif context.project_id != project_id: 

306 raise exception.Forbidden() 

307 

308 

309def authorize_user_context(context, user_id): 

310 """Ensures a request has permission to access the given user.""" 

311 if is_user_context(context): 

312 if not context.user_id: 

313 raise exception.Forbidden() 

314 elif context.user_id != user_id: 

315 raise exception.Forbidden() 

316 

317 

318def authorize_quota_class_context(context, class_name): 

319 """Ensures a request has permission to access the given quota class.""" 

320 if is_user_context(context): 

321 if not context.quota_class: 

322 raise exception.Forbidden() 

323 elif context.quota_class != class_name: 

324 raise exception.Forbidden() 

325 

326 

327def set_target_cell(context, cell_mapping): 

328 """Adds database connection information to the context 

329 for communicating with the given target_cell. 

330 

331 This is used for permanently targeting a cell in a context. 

332 Use this when you want all subsequent code to target a cell. 

333 

334 Passing None for cell_mapping will untarget the context. 

335 

336 :param context: The RequestContext to add connection information 

337 :param cell_mapping: An objects.CellMapping object or None 

338 """ 

339 global CELL_CACHE 

340 if cell_mapping is not None: 

341 # avoid circular import 

342 from nova.db.main import api as db 

343 from nova import rpc 

344 

345 # Synchronize access to the cache by multiple API workers. 

346 @utils.synchronized(cell_mapping.uuid) 

347 def get_or_set_cached_cell_and_set_connections(): 

348 try: 

349 cell_tuple = CELL_CACHE[cell_mapping.uuid] 

350 except KeyError: 

351 db_connection_string = cell_mapping.database_connection 

352 context.db_connection = db.create_context_manager( 

353 db_connection_string) 

354 if not cell_mapping.transport_url.startswith('none'): 

355 context.mq_connection = rpc.create_transport( 

356 cell_mapping.transport_url) 

357 context.cell_uuid = cell_mapping.uuid 

358 CELL_CACHE[cell_mapping.uuid] = (context.db_connection, 

359 context.mq_connection) 

360 else: 

361 context.db_connection = cell_tuple[0] 

362 context.mq_connection = cell_tuple[1] 

363 context.cell_uuid = cell_mapping.uuid 

364 

365 get_or_set_cached_cell_and_set_connections() 

366 else: 

367 context.db_connection = None 

368 context.mq_connection = None 

369 context.cell_uuid = None 

370 

371 

372@contextmanager 

373def target_cell(context, cell_mapping): 

374 """Yields a new context with connection information for a specific cell. 

375 

376 This function yields a copy of the provided context, which is targeted to 

377 the referenced cell for MQ and DB connections. 

378 

379 Passing None for cell_mapping will yield an untargetd copy of the context. 

380 

381 :param context: The RequestContext to add connection information 

382 :param cell_mapping: An objects.CellMapping object or None 

383 """ 

384 # Create a sanitized copy of context by serializing and deserializing it 

385 # (like we would do over RPC). This help ensure that we have a clean 

386 # copy of the context with all the tracked attributes, but without any 

387 # of the hidden/private things we cache on a context. We do this to avoid 

388 # unintentional sharing of cached thread-local data across threads. 

389 # Specifically, this won't include any oslo_db-set transaction context, or 

390 # any existing cell targeting. 

391 cctxt = RequestContext.from_dict(context.to_dict()) 

392 set_target_cell(cctxt, cell_mapping) 

393 yield cctxt 

394 

395 

396def scatter_gather_cells(context, cell_mappings, timeout, fn, *args, **kwargs): 

397 """Target cells in parallel and return their results. 

398 

399 The first parameter in the signature of the function to call for each cell 

400 should be of type RequestContext. 

401 

402 :param context: The RequestContext for querying cells 

403 :param cell_mappings: The CellMappings to target in parallel 

404 :param timeout: The total time in seconds to wait for all the results to be 

405 gathered 

406 :param fn: The function to call for each cell 

407 :param args: The args for the function to call for each cell, not including 

408 the RequestContext 

409 :param kwargs: The kwargs for the function to call for each cell 

410 :returns: A dict {cell_uuid: result} containing the joined results. The 

411 did_not_respond_sentinel will be returned if a cell did not 

412 respond within the timeout. The exception object will 

413 be returned if the call to a cell raised an exception. The 

414 exception will be logged. 

415 """ 

416 greenthreads = [] 

417 queue = eventlet.queue.LightQueue() 

418 results = {} 

419 

420 def gather_result(cell_uuid, fn, *args, **kwargs): 

421 try: 

422 result = fn(*args, **kwargs) 

423 except Exception as e: 

424 # Only log the exception traceback for non-nova exceptions. 

425 if not isinstance(e, exception.NovaException): 

426 LOG.exception('Error gathering result from cell %s', cell_uuid) 

427 result = e 

428 # The queue is already synchronized. 

429 queue.put((cell_uuid, result)) 

430 

431 for cell_mapping in cell_mappings: 

432 with target_cell(context, cell_mapping) as cctxt: 

433 greenthreads.append((cell_mapping.uuid, 

434 utils.spawn(gather_result, cell_mapping.uuid, 

435 fn, cctxt, *args, **kwargs))) 

436 

437 with eventlet.timeout.Timeout(timeout, exception.CellTimeout): 

438 try: 

439 while len(results) != len(greenthreads): 

440 cell_uuid, result = queue.get() 

441 results[cell_uuid] = result 

442 except exception.CellTimeout: 

443 # NOTE(melwitt): We'll fill in did_not_respond_sentinels at the 

444 # same time we kill/wait for the green threads. 

445 pass 

446 

447 # Kill the green threads still pending and wait on those we know are done. 

448 for cell_uuid, greenthread in greenthreads: 

449 if cell_uuid not in results: 

450 greenthread.kill() 

451 results[cell_uuid] = did_not_respond_sentinel 

452 LOG.warning('Timed out waiting for response from cell %s', 

453 cell_uuid) 

454 else: 

455 greenthread.wait() 

456 

457 return results 

458 

459 

460def load_cells(): 

461 global CELLS 

462 if not CELLS: 

463 CELLS = objects.CellMappingList.get_all(get_admin_context()) 

464 LOG.debug('Found %(count)i cells: %(cells)s', 

465 dict(count=len(CELLS), 

466 cells=','.join([c.identity for c in CELLS]))) 

467 

468 if not CELLS: 

469 LOG.error('No cells are configured, unable to continue') 

470 

471 

472def is_cell_failure_sentinel(record): 

473 return (record is did_not_respond_sentinel or 

474 isinstance(record, Exception)) 

475 

476 

477def scatter_gather_skip_cell0(context, fn, *args, **kwargs): 

478 """Target all cells except cell0 in parallel and return their results. 

479 

480 The first parameter in the signature of the function to call for 

481 each cell should be of type RequestContext. There is a timeout for 

482 waiting on all results to be gathered. 

483 

484 :param context: The RequestContext for querying cells 

485 :param fn: The function to call for each cell 

486 :param args: The args for the function to call for each cell, not including 

487 the RequestContext 

488 :param kwargs: The kwargs for the function to call for each cell 

489 :returns: A dict {cell_uuid: result} containing the joined results. The 

490 did_not_respond_sentinel will be returned if a cell did not 

491 respond within the timeout. The exception object will 

492 be returned if the call to a cell raised an exception. The 

493 exception will be logged. 

494 """ 

495 load_cells() 

496 cell_mappings = [cell for cell in CELLS if not cell.is_cell0()] 

497 return scatter_gather_cells(context, cell_mappings, CELL_TIMEOUT, 

498 fn, *args, **kwargs) 

499 

500 

501def scatter_gather_single_cell(context, cell_mapping, fn, *args, **kwargs): 

502 """Target the provided cell and return its results or sentinels in case of 

503 failure. 

504 

505 The first parameter in the signature of the function to call for each cell 

506 should be of type RequestContext. 

507 

508 :param context: The RequestContext for querying cells 

509 :param cell_mapping: The CellMapping to target 

510 :param fn: The function to call for each cell 

511 :param args: The args for the function to call for each cell, not including 

512 the RequestContext 

513 :param kwargs: The kwargs for the function to call for this cell 

514 :returns: A dict {cell_uuid: result} containing the joined results. The 

515 did_not_respond_sentinel will be returned if the cell did not 

516 respond within the timeout. The exception object will 

517 be returned if the call to the cell raised an exception. The 

518 exception will be logged. 

519 """ 

520 return scatter_gather_cells(context, [cell_mapping], CELL_TIMEOUT, fn, 

521 *args, **kwargs) 

522 

523 

524def scatter_gather_all_cells(context, fn, *args, **kwargs): 

525 """Target all cells in parallel and return their results. 

526 

527 The first parameter in the signature of the function to call for 

528 each cell should be of type RequestContext. There is a timeout for 

529 waiting on all results to be gathered. 

530 

531 :param context: The RequestContext for querying cells 

532 :param fn: The function to call for each cell 

533 :param args: The args for the function to call for each cell, not including 

534 the RequestContext 

535 :param kwargs: The kwargs for the function to call for each cell 

536 :returns: A dict {cell_uuid: result} containing the joined results. The 

537 did_not_respond_sentinel will be returned if a cell did not 

538 respond within the timeout. The exception object will 

539 be returned if the call to a cell raised an exception. The 

540 exception will be logged. 

541 """ 

542 load_cells() 

543 return scatter_gather_cells(context, CELLS, CELL_TIMEOUT, 

544 fn, *args, **kwargs)