Files
placement/placement/policy.py
Chris Dent 6fa9eabb79 Stop using global oslo_config
This change was driven out of trying to get nova functional tests
working with an extracted placement, starting with getting the
database fixture cleaner.

Perhaps not surprisingly, trying to share the same 'cfg.CONF' between
two services is rather fraught. Rather than trying to tease out all the
individual issues, which is a very time consuming effort for not much
gain, a different time consuming effort with great gain was tried
instead.

This patch removes the use of the default global cfg.CONF that
oslo_config (optionally) provides and instead ensures that at
the various ways in which one might enter placement: wsgi, cli,
tests, the config is generated and managed in a more explicit
fashion.

Unfortunately this is a large change, but there's no easy way to do it
in incremental chunks without getting very confused and having tests
pass. There are a few classes of changes here, surrounded by various
cleanups to address their addition. Quite a few holes were found in how
config is managed, especially in tests where often we were getting what
we wanted pretty much by accident.

The big changes:

* Importing placement.conf does not automatically register options
  with the global conf. Instead there is a now a register_opts method
  to which a ConfigOpts() is required.

* Because of policy enforcement wanting access to conf, a convenient way
  of having the config pass through context.can() was needed. At
  the start of PlacementHandler (the main dispatch routine) the
  current config (provided to the PlacementHandler at application
  configuration time) is set as an attribute on the RequestContext.
  This is also used where CONF is required in the objects, such as
  randomizing the limited allocation canidates.

* Passing in config to PlacementHandler changes the way the gabbi fixture
  loads the WSGI application. To work around a shortcoming in gabbi
  the fixture needs to CONF global. This is _not_ an
  oslo_config.cfg.CONF global, but something used locally in the fixture
  to set a different config per gabbi test suite.

* The --sql command for alembic commands has been disabled. We don't
  really need that and it would require some messing about with config.
  The command lets you dump raw sql intead of migration files.

* PlacementFixture, for use by nova, has been expanded to create and
  manage its config, database and policy requirements using non-global
  config. It can also accept a previously prepared config.

* The Database fixtures calls 'reset()' in both setUp and cleanUp to be
  certain we are both starting and ending in a known state that will
  not disturb or be disturbed by other tests. This adds confidence (but
  not a guarantee) that in tests that run with eventlet (as in nova)
  things are in more consistent state.

* Configuring the db in the Database fixture is moved into setUp where
  it should have been all along, but is important to be there _after_
  'reset()'.

These of course cascade other changes all over the place. Especially the
need to manually register_opts. There are probably refactorings that can
be done or base classes that can be removed.

Command line tools (e.g. status) which are mostly based on external
libraries continue to use config in the pre-existing way.

A lock fixture for the opportunistic migration tests has been added.
There was a lock fixture previously, provided by oslo_concurrency, but
it, as far as I can tell, requires global config. We don't want that.

Things that will need to be changed as a result of these changes:

* The goals doc at https://review.openstack.org/#/c/618811/ will
  need to be changed to say "keep it this way" rather than "get it
  this way".

Change-Id: Icd629d7cd6d68ca08f9f3b4f0465c3d9a1efeb22
2018-11-30 14:51:23 +00:00

91 lines
3.7 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Policy Enforcement for placement API."""
from oslo_config import cfg
from oslo_log import log as logging
from oslo_policy import policy
from oslo_utils import excutils
from placement import exception
from placement import policies
LOG = logging.getLogger(__name__)
_ENFORCER_PLACEMENT = None
def reset():
"""Used to reset the global _ENFORCER_PLACEMENT between test runs."""
global _ENFORCER_PLACEMENT
if _ENFORCER_PLACEMENT:
_ENFORCER_PLACEMENT.clear()
_ENFORCER_PLACEMENT = None
def init(conf):
"""Init an Enforcer class. Sets the _ENFORCER_PLACEMENT global."""
global _ENFORCER_PLACEMENT
if not _ENFORCER_PLACEMENT:
# NOTE(mriedem): We have to explicitly pass in the
# [placement]/policy_file path because otherwise oslo_policy defaults
# to read the policy file from config option [oslo_policy]/policy_file
# which is used by nova. In other words, to have separate policy files
# for placement and nova, we have to use separate policy_file options.
_ENFORCER_PLACEMENT = policy.Enforcer(
conf, policy_file=conf.placement.policy_file)
_ENFORCER_PLACEMENT.register_defaults(policies.list_rules())
_ENFORCER_PLACEMENT.load_rules()
def get_enforcer():
# This method is used by oslopolicy CLI scripts in order to generate policy
# files from overrides on disk and defaults in code. We can just pass an
# empty list and let oslo do the config lifting for us.
cfg.CONF([], project='placement')
init(cfg.CONF)
return _ENFORCER_PLACEMENT
def authorize(context, action, target, do_raise=True):
"""Verifies that the action is valid on the target in this context.
:param context: instance of placement.context.RequestContext
:param action: string representing the action to be checked
this should be colon separated for clarity, i.e.
``placement:resource_providers:list``
:param target: dictionary representing the object of the action;
for object creation this should be a dictionary representing the
owner of the object e.g. ``{'project_id': context.project_id}``.
:param do_raise: if True (the default), raises PolicyNotAuthorized;
if False, returns False
:raises placement.exception.PolicyNotAuthorized: if verification fails and
do_raise is True.
:returns: non-False value (not necessarily "True") if authorized, and the
exact value False if not authorized and do_raise is False.
"""
init(context.config)
credentials = context.to_policy_values()
try:
# NOTE(mriedem): The "action" kwarg is for the PolicyNotAuthorized exc.
return _ENFORCER_PLACEMENT.authorize(
action, target, credentials, do_raise=do_raise,
exc=exception.PolicyNotAuthorized, action=action)
except policy.PolicyNotRegistered:
with excutils.save_and_reraise_exception():
LOG.exception('Policy not registered')
except Exception:
with excutils.save_and_reraise_exception():
LOG.debug('Policy check for %(action)s failed with credentials '
'%(credentials)s',
{'action': action, 'credentials': credentials})