Coverage for nova/context.py: 85%
203 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« 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.
18"""RequestContext: context for requests that persist through all of nova."""
20from contextlib import contextmanager
21import copy
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
32from nova import exception
33from nova.i18n import _
34from nova import objects
35from nova import policy
36from nova import utils
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
51class _ContextAuthPlugin(plugin.BaseAuthPlugin):
52 """A keystoneauth auth plugin that uses the values from the Context.
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 """
59 def __init__(self, auth_token, sc):
60 super(_ContextAuthPlugin, self).__init__()
62 self.auth_token = auth_token
63 self.service_catalog = ksa_service_catalog.ServiceCatalogV2(sc)
65 def get_token(self, *args, **kwargs):
66 return self.auth_token
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)
76@enginefacade.transaction_context_provider
77class RequestContext(context.RequestContext):
78 """Security context and request information.
80 Represents the user taking a given action within the system.
82 """
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.
92 :param overwrite: Set to False to ensure that the greenthread local
93 copy of the index is not overwritten.
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
103 super(RequestContext, self).__init__(is_admin=is_admin, **kwargs)
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
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 = []
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
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
136 self.user_auth_plugin = user_auth_plugin
137 if self.is_admin is None:
138 self.is_admin = policy.check_is_admin(self)
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)
146 def _get_read_deleted(self):
147 return self._read_deleted
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
155 def _del_read_deleted(self):
156 del self._read_deleted
158 read_deleted = property(_get_read_deleted, _set_read_deleted,
159 _del_read_deleted)
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
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 )
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
213 if 'admin' not in context.roles:
214 context.roles.append('admin')
216 if read_deleted is not None:
217 context.read_deleted = read_deleted
219 return context
221 def can(self, action, target=None, fatal=True):
222 """Verifies that the given action is valid on the target in this
223 context.
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.
233 :raises nova.exception.Forbidden: if verification fails and fatal is
234 True.
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
246 def to_policy_values(self):
247 policy = super(RequestContext, self).to_policy_values()
248 policy['is_admin'] = self.is_admin
249 return policy
251 def __str__(self):
252 return "<Context %s>" % self.to_dict()
255def get_context():
256 """A helper method to get a blank context.
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)
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)
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
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()
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()
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()
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()
327def set_target_cell(context, cell_mapping):
328 """Adds database connection information to the context
329 for communicating with the given target_cell.
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.
334 Passing None for cell_mapping will untarget the context.
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
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
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
372@contextmanager
373def target_cell(context, cell_mapping):
374 """Yields a new context with connection information for a specific cell.
376 This function yields a copy of the provided context, which is targeted to
377 the referenced cell for MQ and DB connections.
379 Passing None for cell_mapping will yield an untargetd copy of the context.
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
396def scatter_gather_cells(context, cell_mappings, timeout, fn, *args, **kwargs):
397 """Target cells in parallel and return their results.
399 The first parameter in the signature of the function to call for each cell
400 should be of type RequestContext.
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 = {}
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))
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)))
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
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()
457 return results
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])))
468 if not CELLS:
469 LOG.error('No cells are configured, unable to continue')
472def is_cell_failure_sentinel(record):
473 return (record is did_not_respond_sentinel or
474 isinstance(record, Exception))
477def scatter_gather_skip_cell0(context, fn, *args, **kwargs):
478 """Target all cells except cell0 in parallel and return their results.
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.
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)
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.
505 The first parameter in the signature of the function to call for each cell
506 should be of type RequestContext.
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)
524def scatter_gather_all_cells(context, fn, *args, **kwargs):
525 """Target all cells in parallel and return their results.
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.
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)