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
This commit is contained in:
Chris Dent 2018-11-20 23:19:28 +00:00
parent 324e4f44da
commit 6fa9eabb79
32 changed files with 278 additions and 265 deletions

View File

@ -69,19 +69,20 @@ def setup_commands():
def main():
CONF = conf.CONF
config = cfg.ConfigOpts()
conf.register_opts(config)
command_opts = setup_commands()
CONF.register_cli_opts(command_opts)
CONF(sys.argv[1:], project='placement',
version=version_info.version_string(),
default_config_files=None)
db_api.configure(CONF)
config.register_cli_opts(command_opts)
config(sys.argv[1:], project='placement',
version=version_info.version_string(),
default_config_files=None)
db_api.configure(config)
try:
func = CONF.command.func
func = config.command.func
return_code = func()
# If return_code ends up None we assume 0.
sys.exit(return_code or 0)
except cfg.NoSuchOptError:
CONF.print_help()
config.print_help()
sys.exit(1)

View File

@ -14,18 +14,19 @@
# under the License.
from __future__ import absolute_import
from oslo_config import cfg
from placement.conf import api
from placement.conf import base
from placement.conf import database
from placement.conf import paths
from placement.conf import placement
CONF = cfg.CONF
api.register_opts(CONF)
base.register_opts(CONF)
database.register_opts(CONF)
paths.register_opts(CONF)
placement.register_opts(CONF)
# To avoid global config, we require an existing ConfigOpts is passed
# to register_opts. Then the caller can have some assurance that the
# config they are using will maintain some independence.
def register_opts(conf):
api.register_opts(conf)
base.register_opts(conf)
database.register_opts(conf)
paths.register_opts(conf)
placement.register_opts(conf)

View File

@ -20,6 +20,10 @@ from placement import policy
@enginefacade.transaction_context_provider
class RequestContext(context.RequestContext):
def __init__(self, *args, **kwargs):
self.config = None
super(RequestContext, self).__init__(*args, **kwargs)
def can(self, action, target=None, fatal=True):
"""Verifies that the given action is valid on the target in this
context.

View File

@ -15,15 +15,14 @@ from __future__ import with_statement
from logging.config import fileConfig
from alembic import context
from oslo_config import cfg
from oslo_db import exception as db_exc
from placement import conf
from placement.db.sqlalchemy import models
from placement import db_api as placement_db
CONF = conf.CONF
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
@ -45,26 +44,6 @@ target_metadata = models.BASE.metadata
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = CONF.placement_database.connection
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
@ -72,12 +51,17 @@ def run_migrations_online():
and associate a connection with the context.
"""
# If CONF and the database are not already configured, set them up. This
# can happen when using the alembic command line tool.
if not CONF.placement_database.connection:
CONF([], project="placement", default_config_files=None)
placement_db.configure(CONF)
connectable = placement_db.get_placement_engine()
try:
connectable = placement_db.get_placement_engine()
except db_exc.CantStartEngineError:
# We are being called from a context where the database hasn't been
# configured so we need to set up Config and config the database.
# This is usually the alembic command line.
config = cfg.ConfigOpts()
conf.register_opts(config)
config([], project="placement", default_config_files=None)
placement_db.configure(config)
connectable = placement_db.get_placement_engine()
with connectable.connect() as connection:
context.configure(
@ -87,7 +71,8 @@ def run_migrations_online():
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
raise Exception('offline mode disabled')
else:
run_migrations_online()

View File

@ -57,7 +57,7 @@ def deploy(conf):
fault_middleware = fault_wrap.FaultWrapper
request_log = requestlog.RequestLog
application = handler.PlacementHandler()
application = handler.PlacementHandler(config=conf)
# configure microversion middleware in the old school way
application = microversion_middleware(
application, microversion.SERVICE_TYPE, microversion.VERSIONS,

View File

@ -192,10 +192,13 @@ class PlacementHandler(object):
"""
def __init__(self, **local_config):
# NOTE(cdent): Local config currently unused.
self._map = make_map(ROUTE_DECLARATIONS)
self.config = local_config['config']
def __call__(self, environ, start_response):
# set a reference to the oslo.config ConfigOpts on the RequestContext
context = environ['placement.context']
context.config = self.config
# Check that an incoming request with a content-length header
# that is an integer > 0 and not empty, also has a content-type
# header that is not empty. If not raise a 400.

View File

@ -11,7 +11,6 @@
# under the License.
"""DB Utility methods for placement."""
from oslo_config import cfg
from oslo_log import log as logging
import webob
@ -23,7 +22,6 @@ from placement.objects import project as project_obj
from placement.objects import user as user_obj
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
@ -54,8 +52,8 @@ def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
created_new_consumer = False
requires_consumer_generation = want_version.matches((1, 28))
if project_id is None:
project_id = CONF.placement.incomplete_consumer_project_id
user_id = CONF.placement.incomplete_consumer_user_id
project_id = ctx.config.placement.incomplete_consumer_project_id
user_id = ctx.config.placement.incomplete_consumer_user_id
try:
proj = project_obj.Project.get_by_external_id(ctx, project_id)
except exception.NotFound:

View File

@ -10,7 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_versionedobjects import base
from oslo_versionedobjects import fields
@ -20,7 +19,6 @@ from placement.db.sqlalchemy import models
from placement import db_api
from placement import exception
CONF = cfg.CONF
PROJECT_TBL = models.Project.__table__
@ -29,7 +27,7 @@ def ensure_incomplete_project(ctx):
"""Ensures that a project record is created for the "incomplete consumer
project". Returns the internal ID of that record.
"""
incomplete_id = CONF.placement.incomplete_consumer_project_id
incomplete_id = ctx.config.placement.incomplete_consumer_project_id
sel = sa.select([PROJECT_TBL.c.id]).where(
PROJECT_TBL.c.external_id == incomplete_id)
res = ctx.session.execute(sel).fetchone()

View File

@ -23,7 +23,6 @@ import random
import os_traits
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_db import api as oslo_db_api
from oslo_db import exception as db_exc
from oslo_log import log as logging
@ -63,7 +62,6 @@ _RC_CACHE = None
_TRAIT_LOCK = 'trait_sync'
_TRAITS_SYNCED = False
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
@ -4014,9 +4012,9 @@ class AllocationCandidates(base.VersionedObject):
"""Returns an AllocationCandidates object containing all resource
providers matching a set of supplied resource constraints, with a set
of allocation requests constructed from that list of resource
providers. If CONF.placement.randomize_allocation_candidates is True
(default is False) then the order of the allocation requests will
be randomized.
providers. If CONF.placement.randomize_allocation_candidates (on
contex.config) is True (default is False) then the order of the
allocation requests will be randomized.
:param context: Nova RequestContext.
:param requests: Dict, keyed by suffix, of placement.lib.RequestGroup
@ -4159,16 +4157,17 @@ class AllocationCandidates(base.VersionedObject):
alloc_request_objs, summary_objs = _merge_candidates(
candidates, group_policy=group_policy)
return cls._limit_results(alloc_request_objs, summary_objs, limit)
return cls._limit_results(context, alloc_request_objs, summary_objs,
limit)
@staticmethod
def _limit_results(alloc_request_objs, summary_objs, limit):
def _limit_results(context, alloc_request_objs, summary_objs, limit):
# Limit the number of allocation request objects. We do this after
# creating all of them so that we can do a random slice without
# needing to mess with the complex sql above or add additional
# columns to the DB.
if limit and limit < len(alloc_request_objs):
if CONF.placement.randomize_allocation_candidates:
if context.config.placement.randomize_allocation_candidates:
alloc_request_objs = random.sample(alloc_request_objs, limit)
else:
alloc_request_objs = alloc_request_objs[:limit]
@ -4187,7 +4186,7 @@ class AllocationCandidates(base.VersionedObject):
continue
kept_summary_objs.append(summary)
summary_objs = kept_summary_objs
elif CONF.placement.randomize_allocation_candidates:
elif context.config.placement.randomize_allocation_candidates:
random.shuffle(alloc_request_objs)
return alloc_request_objs, summary_objs

View File

@ -10,7 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_versionedobjects import base
from oslo_versionedobjects import fields
@ -20,7 +19,6 @@ from placement.db.sqlalchemy import models
from placement import db_api
from placement import exception
CONF = cfg.CONF
USER_TBL = models.User.__table__
@ -29,7 +27,7 @@ def ensure_incomplete_user(ctx):
"""Ensures that a user record is created for the "incomplete consumer
user". Returns the internal ID of that record.
"""
incomplete_id = CONF.placement.incomplete_consumer_user_id
incomplete_id = ctx.config.placement.incomplete_consumer_user_id
sel = sa.select([USER_TBL.c.id]).where(
USER_TBL.c.external_id == incomplete_id)
res = ctx.session.execute(sel).fetchone()

View File

@ -20,7 +20,6 @@ from placement import exception
from placement import policies
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
_ENFORCER_PLACEMENT = None
@ -33,7 +32,7 @@ def reset():
_ENFORCER_PLACEMENT = None
def init():
def init(conf):
"""Init an Enforcer class. Sets the _ENFORCER_PLACEMENT global."""
global _ENFORCER_PLACEMENT
if not _ENFORCER_PLACEMENT:
@ -43,7 +42,7 @@ def init():
# 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)
conf, policy_file=conf.placement.policy_file)
_ENFORCER_PLACEMENT.register_defaults(policies.list_rules())
_ENFORCER_PLACEMENT.load_rules()
@ -53,7 +52,7 @@ def get_enforcer():
# 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()
init(cfg.CONF)
return _ENFORCER_PLACEMENT
@ -74,7 +73,7 @@ def authorize(context, action, target, do_raise=True):
:returns: non-False value (not necessarily "True") if authorized, and the
exact value False if not authorized and do_raise is False.
"""
init()
init(context.config)
credentials = context.to_policy_values()
try:
# NOTE(mriedem): The "action" kwarg is for the PolicyNotAuthorized exc.

View File

@ -14,11 +14,14 @@
# License for the specific language governing permissions and limitations
# under the License.
"""Fixtures for Nova tests."""
"""Fixtures for Placement tests."""
from __future__ import absolute_import
import tempfile
import fixtures
from oslo_concurrency.fixture import lockutils as lock_fixture
from oslo_concurrency import lockutils
from oslo_config import cfg
from placement.db.sqlalchemy import migration
@ -27,16 +30,10 @@ from placement import deploy
from placement.objects import resource_provider
CONF = cfg.CONF
session_configured = False
def reset():
"""Call this to allow the placement db fixture to be reconfigured
in the same process.
"""
global session_configured
session_configured = False
placement_db.placement_context_manager.dispose_pool()
# TODO(cdent): Future handling in sqlalchemy may allow doing this
# in a less hacky way.
@ -46,26 +43,25 @@ def reset():
class Database(fixtures.Fixture):
def __init__(self, set_config=False):
def __init__(self, conf_fixture, set_config=False):
"""Create a database fixture."""
super(Database, self).__init__()
global session_configured
if not session_configured:
if set_config:
try:
CONF.register_opt(cfg.StrOpt('connection'),
group='placement_database')
except cfg.DuplicateOptError:
# already registered
pass
CONF.set_override('connection', 'sqlite://',
group='placement_database')
placement_db.configure(CONF)
session_configured = True
if set_config:
try:
conf_fixture.register_opt(
cfg.StrOpt('connection'), group='placement_database')
except cfg.DuplicateOptError:
# already registered
pass
conf_fixture.config(connection='sqlite://',
group='placement_database')
self.conf_fixture = conf_fixture
self.get_engine = placement_db.get_placement_engine
def setUp(self):
super(Database, self).setUp()
reset()
placement_db.configure(self.conf_fixture.conf)
migration.create_schema()
resource_provider._TRAITS_SYNCED = False
resource_provider._RC_CACHE = None
@ -76,3 +72,15 @@ class Database(fixtures.Fixture):
reset()
resource_provider._TRAITS_SYNCED = False
resource_provider._RC_CACHE = None
class ExternalLockFixture(lock_fixture.LockFixture):
"""Provide a predictable inter-process file-based lock that doesn't
require oslo.config, by setting its own lock_path.
This is used to prevent live database test from conflicting with
one another in a concurrent enviornment.
"""
def __init__(self, name):
lock_path = tempfile.gettempdir()
self.mgr = lockutils.lock(name, external=True, lock_path=lock_path)

View File

@ -16,15 +16,13 @@ from oslo_log.fixture import logging_error
from oslotest import output
import testtools
from placement import conf
from placement import context
from placement.tests import fixtures
from placement.tests.functional.fixtures import capture
from placement.tests.unit import policy_fixture
CONF = cfg.CONF
class TestCase(testtools.TestCase):
"""A base test case for placement functional tests.
@ -36,14 +34,14 @@ class TestCase(testtools.TestCase):
super(TestCase, self).setUp()
# Manage required configuration
conf_fixture = self.useFixture(config_fixture.Config(CONF))
conf_fixture.config(
group='placement_database',
connection='sqlite://',
sqlite_synchronous=False)
CONF([], default_config_files=[])
self.conf_fixture = self.useFixture(
config_fixture.Config(cfg.ConfigOpts()))
conf.register_opts(self.conf_fixture.conf)
self.placement_db = self.useFixture(fixtures.Database(
self.conf_fixture, set_config=True))
self.conf_fixture.conf([], default_config_files=[])
self.useFixture(policy_fixture.PolicyFixture())
self.useFixture(policy_fixture.PolicyFixture(self.conf_fixture))
self.useFixture(capture.Logging())
self.useFixture(output.CaptureOutput())
@ -51,5 +49,5 @@ class TestCase(testtools.TestCase):
self.useFixture(capture.WarningsFixture())
self.useFixture(logging_error.get_logging_handle_error_fixture())
self.placement_db = self.useFixture(fixtures.Database())
self.context = context.RequestContext()
self.context.config = self.conf_fixture.conf

View File

@ -10,7 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import os_traits
from oslo_config import cfg
from oslo_utils.fixture import uuidsentinel as uuids
import six
import sqlalchemy as sa
@ -22,9 +21,6 @@ from placement import rc_fields as fields
from placement.tests.functional.db import test_base as tb
CONF = cfg.CONF
class ProviderDBHelperTestCase(tb.PlacementDbBaseTestCase):
def test_get_provider_ids_matching(self):
@ -705,8 +701,8 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
# Do it again, with conf set to randomize. We can't confirm the
# random-ness but we can be sure the code path doesn't explode.
CONF.set_override('randomize_allocation_candidates', True,
group='placement')
self.conf_fixture.config(randomize_allocation_candidates=True,
group='placement')
# Ask for two candidates.
limit = 2

View File

@ -10,7 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from oslo_utils.fixture import uuidsentinel as uuids
import sqlalchemy as sa
@ -25,7 +24,6 @@ from placement.tests.functional import base
from placement.tests.functional.db import test_base as tb
CONF = cfg.CONF
CONSUMER_TBL = consumer_obj.CONSUMER_TBL
PROJECT_TBL = project_obj.PROJECT_TBL
USER_TBL = user_obj.USER_TBL
@ -137,7 +135,8 @@ class CreateIncompleteConsumersTestCase(base.TestCase):
@db_api.placement_context_manager.reader
def _check_incomplete_consumers(self, ctx):
incomplete_project_id = CONF.placement.incomplete_consumer_project_id
config = ctx.config
incomplete_project_id = config.placement.incomplete_consumer_project_id
# Verify we have a record in projects for the missing sentinel
sel = PROJECT_TBL.select(
@ -147,7 +146,7 @@ class CreateIncompleteConsumersTestCase(base.TestCase):
incomplete_proj_id = rec['id']
# Verify we have a record in users for the missing sentinel
incomplete_user_id = CONF.placement.incomplete_consumer_user_id
incomplete_user_id = config.placement.incomplete_consumer_user_id
sel = user_obj.USER_TBL.select(
USER_TBL.c.external_id == incomplete_user_id)
rec = ctx.session.execute(sel).first()

View File

@ -25,11 +25,10 @@ the tests.
import contextlib
import functools
import tempfile
from alembic import script
import mock
from oslo_concurrency.fixture import lockutils as concurrency
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_db import exception as db_exc
from oslo_db.sqlalchemy import enginefacade
@ -46,7 +45,6 @@ from placement import db_api
from placement.tests import fixtures as db_fixture
CONF = conf.CONF
DB_NAME = 'openstack_citest'
LOG = logging.getLogger(__name__)
@ -64,8 +62,7 @@ def configure(conf_fixture, db_url):
here, not done as a base class as the mess of mixins makes that
inscrutable. So instead we create a nice simple function.
"""
conf_fixture.config(lock_path=tempfile.gettempdir(),
group='oslo_concurrency')
conf.register_opts(conf_fixture.conf)
conf_fixture.config(group='placement_database', connection=db_url)
# We need to retry at least once (and quickly) otherwise the connection
# test routines in oslo_db do not run, and the exception handling for
@ -192,11 +189,11 @@ class MigrationCheckersMixin(object):
def setUp(self):
self.addCleanup(db_fixture.reset)
db_url = generate_url(self.DRIVER)
conf_fixture = self.useFixture(config_fixture.Config(CONF))
conf_fixture = self.useFixture(config_fixture.Config(cfg.ConfigOpts()))
configure(conf_fixture, db_url)
self.useFixture(concurrency.LockFixture('test_mig'))
self.useFixture(db_fixture.ExternalLockFixture('test_mig'))
db_fixture.reset()
db_api.configure(CONF)
db_api.configure(conf_fixture.conf)
try:
self.engine = db_api.get_placement_engine()
except (db_exc.DBNonExistentDatabase, db_exc.DBConnectionError):
@ -261,11 +258,11 @@ class TestMigrationsPostgresql(MigrationCheckersMixin,
class ModelsMigrationSyncMixin(object):
def setUp(self):
url = generate_url(self.DRIVER)
conf_fixture = self.useFixture(config_fixture.Config(CONF))
conf_fixture = self.useFixture(config_fixture.Config(cfg.ConfigOpts()))
configure(conf_fixture, url)
self.useFixture(concurrency.LockFixture('test_mig'))
self.useFixture(db_fixture.ExternalLockFixture('test_mig'))
db_fixture.reset()
db_api.configure(CONF)
db_api.configure(conf_fixture.conf)
super(ModelsMigrationSyncMixin, self).setUp()
# This is required to prevent the global opportunistic db settings
# leaking into other tests.

View File

@ -23,6 +23,7 @@ from oslo_utils.fixture import uuidsentinel as uuids
from oslo_utils import uuidutils
from oslotest import output
from placement import conf
from placement import context
from placement import deploy
from placement.objects import project as project_obj
@ -36,10 +37,14 @@ from placement.tests.functional.fixtures import capture
from placement.tests.unit import policy_fixture
CONF = cfg.CONF
# This global conf is not a global olso_config.cfg.CONF. It's a global
# used locally to work around a limitation in the way that gabbi instantiates
# the WSGI application being tested.
CONF = None
def setup_app():
global CONF
return deploy.loadapp(CONF)
@ -47,6 +52,7 @@ class APIFixture(fixture.GabbiFixture):
"""Setup the required backend fixtures for a basic placement service."""
def start_fixture(self):
global CONF
# Set up stderr and stdout captures by directly driving the
# existing nova fixtures that do that. This captures the
# output that happens outside individual tests (for
@ -62,31 +68,35 @@ class APIFixture(fixture.GabbiFixture):
self.warnings_fixture = capture.WarningsFixture()
self.warnings_fixture.setUp()
self.conf_fixture = config_fixture.Config(CONF)
# Do not use global CONF
self.conf_fixture = config_fixture.Config(cfg.ConfigOpts())
self.conf_fixture.setUp()
self.conf_fixture.config(
group="placement_database",
connection='sqlite://',
sqlite_synchronous=False)
conf.register_opts(self.conf_fixture.conf)
self.conf_fixture.config(group='api', auth_strategy='noauth2')
self.placement_db_fixture = fixtures.Database(
self.conf_fixture, set_config=True)
self.placement_db_fixture.setUp()
self.context = context.RequestContext()
# Register CORS opts, but do not set config. This has the
# effect of exercising the "don't use cors" path in
# deploy.py. Without setting some config the group will not
# be present.
CONF.register_opts(cors.CORS_OPTS, 'cors')
self.conf_fixture.register_opts(cors.CORS_OPTS, 'cors')
# Set default policy opts, otherwise the deploy module can
# NoSuchOptError.
policy_opts.set_defaults(CONF)
policy_opts.set_defaults(self.conf_fixture.conf)
# Make sure default_config_files is an empty list, not None.
# If None /etc/nova/nova.conf is read and confuses results.
CONF([], default_config_files=[])
# If None /etc/placement/placement.conf is read and confuses results.
self.conf_fixture.conf([], default_config_files=[])
self.placement_db_fixture = fixtures.Database()
self.placement_db_fixture.setUp()
# Turn on a policy fixture.
self.policy_fixture = policy_fixture.PolicyFixture(
self.conf_fixture)
self.policy_fixture.setUp()
os.environ['RP_UUID'] = uuidutils.generate_uuid()
os.environ['RP_NAME'] = uuidutils.generate_uuid()
@ -100,14 +110,18 @@ class APIFixture(fixture.GabbiFixture):
os.environ['CONSUMER_UUID'] = uuidutils.generate_uuid()
os.environ['PARENT_PROVIDER_UUID'] = uuidutils.generate_uuid()
os.environ['ALT_PARENT_PROVIDER_UUID'] = uuidutils.generate_uuid()
CONF = self.conf_fixture.conf
def stop_fixture(self):
global CONF
self.placement_db_fixture.cleanUp()
self.warnings_fixture.cleanUp()
self.output_stream_fixture.cleanUp()
self.standard_logging_fixture.cleanUp()
self.logging_error_fixture.cleanUp()
self.policy_fixture.cleanUp()
self.conf_fixture.cleanUp()
CONF = None
class AllocationFixture(APIFixture):
@ -476,8 +490,6 @@ class OpenPolicyFixture(APIFixture):
def start_fixture(self):
super(OpenPolicyFixture, self).start_fixture()
self.placement_policy_fixture = policy_fixture.PolicyFixture()
self.placement_policy_fixture.setUp()
# Get all of the registered rules and set them to '@' to allow any
# user to have access. The nova policy "admin_or_owner" concept does
# not really apply to most of placement resources since they do not
@ -489,8 +501,7 @@ class OpenPolicyFixture(APIFixture):
if name in ['placement', 'admin_api']:
continue
rules[name] = '@'
self.placement_policy_fixture.set_rules(rules)
self.policy_fixture.set_rules(rules)
def stop_fixture(self):
super(OpenPolicyFixture, self).stop_fixture()
self.placement_policy_fixture.cleanUp()

View File

@ -14,14 +14,14 @@ from __future__ import absolute_import
import fixtures
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_policy import opts as policy_opts
from oslo_utils import uuidutils
from wsgi_intercept import interceptor
from placement import conf
from placement import deploy
from placement.tests import fixtures as db_fixture
CONF = cfg.CONF
from placement.tests.unit import policy_fixture
class PlacementFixture(fixtures.Fixture):
@ -34,22 +34,33 @@ class PlacementFixture(fixtures.Fixture):
all calls would be passing this token.
This fixture takes care of starting a fixture for an in-RAM placement
database, unless the db kwargs is False.
database, unless the db kwarg is False.
Used by other services, including nova, for functional tests.
"""
def __init__(self, token='admin', db=True):
def __init__(self, token='admin', conf_fixture=None, db=True):
self.token = token
self.db = db
self.conf_fixture = conf_fixture
def setUp(self):
super(PlacementFixture, self).setUp()
if self.db:
self.useFixture(db_fixture.Database(set_config=True))
if not self.conf_fixture:
config = cfg.ConfigOpts()
self.conf_fixture = self.useFixture(config_fixture.Config(config))
conf.register_opts(self.conf_fixture.conf)
conf_fixture = config_fixture.Config(CONF)
conf_fixture.config(group='api', auth_strategy='noauth2')
loader = deploy.loadapp(CONF)
if self.db:
self.useFixture(db_fixture.Database(self.conf_fixture,
set_config=True))
policy_opts.set_defaults(self.conf_fixture.conf)
self.conf_fixture.config(group='api', auth_strategy='noauth2')
self.conf_fixture.conf([], default_config_files=[])
self.useFixture(policy_fixture.PolicyFixture(self.conf_fixture))
loader = deploy.loadapp(self.conf_fixture.conf)
app = lambda: loader
self.endpoint = 'http://%s/placement' % uuidutils.generate_uuid()
intercept = interceptor.RequestsInterceptor(app, url=self.endpoint)

View File

@ -11,26 +11,31 @@
# under the License.
from oslo_config import cfg
from oslo_policy import opts as policy_opts
from oslo_utils.fixture import uuidsentinel
from placement import conf
from placement import direct
from placement.tests.functional import base
CONF = cfg.CONF
class TestDirect(base.TestCase):
def setUp(self):
super(TestDirect, self).setUp()
self.conf = cfg.ConfigOpts()
conf.register_opts(self.conf)
policy_opts.set_defaults(self.conf)
def test_direct_is_there(self):
with direct.PlacementDirect(CONF) as client:
with direct.PlacementDirect(self.conf) as client:
resp = client.get('/')
self.assertTrue(resp)
data = resp.json()
self.assertEqual('v1.0', data['versions'][0]['id'])
def test_get_resource_providers(self):
with direct.PlacementDirect(CONF) as client:
with direct.PlacementDirect(self.conf) as client:
resp = client.get('/resource_providers')
self.assertTrue(resp)
data = resp.json()
@ -38,7 +43,7 @@ class TestDirect(base.TestCase):
def test_create_resource_provider(self):
data = {'name': 'fake'}
with direct.PlacementDirect(CONF) as client:
with direct.PlacementDirect(self.conf) as client:
resp = client.post('/resource_providers', json=data)
self.assertTrue(resp)
resp = client.get('/resource_providers')
@ -48,13 +53,13 @@ class TestDirect(base.TestCase):
def test_json_validation_happens(self):
data = {'name': 'fake', 'cowsay': 'moo'}
with direct.PlacementDirect(CONF) as client:
with direct.PlacementDirect(self.conf) as client:
resp = client.post('/resource_providers', json=data)
self.assertFalse(resp)
self.assertEqual(400, resp.status_code)
def test_microversion_handling(self):
with direct.PlacementDirect(CONF) as client:
with direct.PlacementDirect(self.conf) as client:
# create parent
parent_data = {'name': uuidsentinel.p_rp,
'uuid': uuidsentinel.p_rp}

View File

@ -10,16 +10,11 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from placement import direct
from placement import handler
from placement.tests.functional import base
CONF = cfg.CONF
class TestVerifyPolicy(base.TestCase):
"""Verify that all defined placement routes have a policy."""
@ -42,7 +37,8 @@ class TestVerifyPolicy(base.TestCase):
(method, route, response.status_code))
def test_verify_policy(self):
with direct.PlacementDirect(CONF, latest_microversion=True) as client:
conf = self.conf_fixture.conf
with direct.PlacementDirect(conf, latest_microversion=True) as client:
for route, methods in handler.ROUTE_DECLARATIONS.items():
if route in self.EXCEPTIONS:
continue

View File

@ -19,15 +19,17 @@ import six
import testtools
from placement.cmd import manage
from placement import conf
class TestCommandParsers(testtools.TestCase):
def setUp(self):
super(TestCommandParsers, self).setUp()
self.conf = cfg.CONF
self.conf = cfg.ConfigOpts()
conf_fixture = config_fixture.Config(self.conf)
self.useFixture(conf_fixture)
conf.register_opts(conf_fixture.conf)
# Quiet output from argparse (used within oslo_config).
# If you are debugging, commenting this out might be useful.
self.output = self.useFixture(

View File

@ -14,12 +14,14 @@
import fixtures
import microversion_parse
import mock
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_utils.fixture import uuidsentinel
import testtools
import webob
from placement import conf
from placement import context
from placement import exception
from placement.handlers import util
from placement import microversion
@ -28,12 +30,12 @@ from placement.objects import project as project_obj
from placement.objects import user as user_obj
CONF = conf.CONF
class TestEnsureConsumer(testtools.TestCase):
def setUp(self):
super(TestEnsureConsumer, self).setUp()
self.conf = cfg.ConfigOpts()
self.useFixture(config_fixture.Config(self.conf))
conf.register_opts(self.conf)
self.mock_project_get = self.useFixture(fixtures.MockPatch(
'placement.objects.project.'
'Project.get_by_external_id')).mock
@ -52,7 +54,8 @@ class TestEnsureConsumer(testtools.TestCase):
self.mock_consumer_create = self.useFixture(fixtures.MockPatch(
'placement.objects.consumer.'
'Consumer.create')).mock
self.ctx = mock.sentinel.ctx
self.ctx = context.RequestContext(user_id='fake', project_id='fake')
self.ctx.config = self.conf
self.consumer_id = uuidsentinel.consumer
self.project_id = uuidsentinel.project
self.user_id = uuidsentinel.user
@ -144,9 +147,9 @@ class TestEnsureConsumer(testtools.TestCase):
consumer_gen, self.before_version)
self.mock_project_get.assert_called_once_with(
self.ctx, CONF.placement.incomplete_consumer_project_id)
self.ctx, self.conf.placement.incomplete_consumer_project_id)
self.mock_user_get.assert_called_once_with(
self.ctx, CONF.placement.incomplete_consumer_user_id)
self.ctx, self.conf.placement.incomplete_consumer_user_id)
self.mock_consumer_get.assert_called_once_with(
self.ctx, self.consumer_id)
self.mock_project_create.assert_called_once()

View File

@ -11,11 +11,14 @@
# under the License.
import mock
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_utils.fixture import uuidsentinel as uuids
from oslo_utils import timeutils
import six
import testtools
from placement import conf
from placement import context
from placement import exception
from placement.objects import resource_provider
@ -103,6 +106,10 @@ class _TestCase(testtools.TestCase):
self.user_id = 'fake-user'
self.project_id = 'fake-project'
self.context = context.RequestContext(self.user_id, self.project_id)
config = cfg.ConfigOpts()
self.conf_fixture = self.useFixture(config_fixture.Config(config))
conf.register_opts(config)
self.context.config = config
class TestResourceProviderNoDB(_TestCase):
@ -356,6 +363,6 @@ class TestAllocationCandidatesNoDB(_TestCase):
sum6 = mock.Mock(resource_provider=mock.Mock(uuid=6))
sum_in = [sum1, sum0, sum4, sum8, sum5, sum7, sum6]
aro, sum = resource_provider.AllocationCandidates._limit_results(
aro_in, sum_in, 2)
self.context, aro_in, sum_in, 2)
self.assertEqual(aro_in[:2], aro)
self.assertEqual(set([sum1, sum0, sum4, sum8, sum5]), set(sum))

View File

@ -14,8 +14,6 @@
import fixtures
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_policy import policy as oslo_policy
from placement.conf import paths
@ -23,15 +21,19 @@ from placement import policy as placement_policy
class PolicyFixture(fixtures.Fixture):
def __init__(self, conf_fixture):
self.conf_fixture = conf_fixture
super(PolicyFixture, self).__init__()
"""Load the default placement policy for tests."""
def setUp(self):
super(PolicyFixture, self).setUp()
self.conf_fixture = self.useFixture(config_fixture.Config(cfg.CONF))
policy_file = paths.state_path_def(
'etc/placement/placement-policy.yaml')
self.conf_fixture.config(group='placement', policy_file=policy_file)
placement_policy.reset()
placement_policy.init()
placement_policy.init(self.conf_fixture.conf)
self.addCleanup(placement_policy.reset)
@staticmethod

View File

@ -17,17 +17,17 @@ import testtools
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from placement import conf
from placement import db_api
CONF = cfg.CONF
class DbApiTests(testtools.TestCase):
def setUp(self):
super(DbApiTests, self).setUp()
self.conf_fixture = self.useFixture(config_fixture.Config(CONF))
config = cfg.ConfigOpts()
self.conf_fixture = self.useFixture(config_fixture.Config(config))
conf.register_opts(self.conf_fixture.conf)
db_api.configure.reset()
@mock.patch.object(db_api.placement_context_manager, "configure")

View File

@ -16,8 +16,7 @@ import testtools
from oslo_config import cfg
from oslo_config import fixture as config_fixture
CONF = cfg.CONF
from placement import conf
class TestPlacementDBConf(testtools.TestCase):
@ -25,7 +24,9 @@ class TestPlacementDBConf(testtools.TestCase):
def setUp(self):
super(TestPlacementDBConf, self).setUp()
self.conf_fixture = self.useFixture(config_fixture.Config(CONF))
config = cfg.ConfigOpts()
self.conf_fixture = self.useFixture(config_fixture.Config(config))
conf.register_opts(config)
def test_missing_config_raises(self):
"""Not setting [placement_database]/connection is an error."""

View File

@ -13,31 +13,39 @@
# under the License.
"""Unit tests for the deply function used to build the Placement service."""
from keystonemiddleware import auth_token
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_policy import opts as policy_opts
import testtools
import webob
from placement import conf
from placement import deploy
CONF = cfg.CONF
class DeployTest(testtools.TestCase):
def test_auth_middleware_factory(self):
"""Make sure that configuration settings make their way to
the keystone middleware correctly.
"""
config = cfg.ConfigOpts()
conf_fixture = self.useFixture(config_fixture.Config(config))
conf.register_opts(conf_fixture.conf)
# NOTE(cdent): There appears to be no simple way to get the list of
# options used by the auth_token middleware. So we pull from an
# existing data structure.
auth_token_opts = auth_token.AUTH_TOKEN_OPTS[0][1]
conf_fixture.register_opts(auth_token_opts, group='keystone_authtoken')
www_authenticate_uri = 'http://example.com/identity'
CONF.set_override('www_authenticate_uri', www_authenticate_uri,
conf_fixture.config(www_authenticate_uri=www_authenticate_uri,
group='keystone_authtoken')
# ensure that the auth_token middleware is chosen
CONF.set_override('auth_strategy', 'keystone', group='api')
conf_fixture.config(auth_strategy='keystone', group='api')
# register and default policy opts (referenced by deploy)
policy_opts.set_defaults(CONF)
app = deploy.deploy(CONF)
policy_opts.set_defaults(conf_fixture.conf)
app = deploy.deploy(conf_fixture.conf)
req = webob.Request.blank('/resource_providers', method="GET")
response = req.get_response(app)

View File

@ -128,10 +128,11 @@ class PlacementLoggingTest(testtools.TestCase):
@mock.patch("placement.handler.LOG")
def test_404_no_error_log(self, mocked_log):
environ = _environ(path='/hello', method='GET')
config = mock.MagicMock()
context_mock = mock.Mock()
context_mock.to_policy_values.return_value = {'roles': ['admin']}
environ['placement.context'] = context_mock
app = handler.PlacementHandler()
app = handler.PlacementHandler(config=config)
self.assertRaises(webob.exc.HTTPNotFound,
app, environ, start_response)
mocked_log.error.assert_not_called()
@ -160,7 +161,9 @@ class ContentHeadersTest(testtools.TestCase):
def setUp(self):
super(ContentHeadersTest, self).setUp()
self.environ = _environ(path='/')
self.app = handler.PlacementHandler()
config = mock.MagicMock()
self.environ['placement.context'] = mock.MagicMock()
self.app = handler.PlacementHandler(config=config)
def test_no_content_type(self):
self.environ['CONTENT_LENGTH'] = '10'

View File

@ -12,32 +12,33 @@
import os
import fixtures
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_policy import policy as oslo_policy
import testtools
from placement import conf
from placement import context
from placement import exception
from placement import policy
from placement.tests.unit import policy_fixture
from placement import util
CONF = cfg.CONF
class PlacementPolicyTestCase(testtools.TestCase):
"""Tests interactions with placement policy."""
def setUp(self):
super(PlacementPolicyTestCase, self).setUp()
self.conf_fixture = self.useFixture(config_fixture.Config(CONF))
config = cfg.ConfigOpts()
self.conf_fixture = self.useFixture(config_fixture.Config(config))
conf.register_opts(config)
self.ctxt = context.RequestContext(user_id='fake', project_id='fake')
self.target = {'user_id': 'fake', 'project_id': 'fake'}
# A value is required in the database connection opt for CONF to
# A value is required in the database connection opt for conf to
# parse.
CONF.set_default('connection', 'stub', group='placement_database')
CONF([], default_config_files=[])
self.conf_fixture.config(connection='stub', group='placement_database')
config([], default_config_files=[])
self.ctxt.config = config
policy.reset()
self.addCleanup(policy.reset)
@ -46,39 +47,40 @@ class PlacementPolicyTestCase(testtools.TestCase):
authorizations against a fake rule between updates to the physical
policy file.
"""
with util.tempdir() as tmpdir:
tmpfilename = os.path.join(tmpdir, 'placement-policy.yaml')
tempdir = self.useFixture(fixtures.TempDir())
tmpfilename = os.path.join(tempdir.path, 'placement-policy.yaml')
self.conf_fixture.config(
group='placement', policy_file=tmpfilename)
self.conf_fixture.config(
group='placement', policy_file=tmpfilename)
action = 'placement:test'
# Expect PolicyNotRegistered since defaults are not yet loaded.
self.assertRaises(oslo_policy.PolicyNotRegistered,
policy.authorize, self.ctxt, action, self.target)
action = 'placement:test'
# Expect PolicyNotRegistered since defaults are not yet loaded.
self.assertRaises(oslo_policy.PolicyNotRegistered,
policy.authorize, self.ctxt, action, self.target)
# Load the default action and rule (defaults to "any").
enforcer = policy.get_enforcer()
rule = oslo_policy.RuleDefault(action, '')
enforcer.register_default(rule)
# Load the default action and rule (defaults to "any").
enforcer = policy.get_enforcer()
rule = oslo_policy.RuleDefault(action, '')
enforcer.register_default(rule)
# Now auth should work because the action is registered and anyone
# can perform the action.
policy.authorize(self.ctxt, action, self.target)
# Now auth should work because the action is registered and anyone
# can perform the action.
policy.authorize(self.ctxt, action, self.target)
# Now update the policy file and reload it to disable the action
# from all users.
with open(tmpfilename, "w") as policyfile:
policyfile.write('"%s": "!"' % action)
enforcer.load_rules(force_reload=True)
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
self.ctxt, action, self.target)
# Now update the policy file and reload it to disable the action
# from all users.
with open(tmpfilename, "w") as policyfile:
policyfile.write('"%s": "!"' % action)
enforcer.load_rules(force_reload=True)
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
self.ctxt, action, self.target)
def test_authorize_do_raise_false(self):
"""Tests that authorize does not raise an exception when the check
fails.
"""
fixture = self.useFixture(policy_fixture.PolicyFixture())
fixture = self.useFixture(
policy_fixture.PolicyFixture(self.conf_fixture))
fixture.set_rules({'placement': '!'})
self.assertFalse(
policy.authorize(

View File

@ -18,7 +18,6 @@ import datetime
import fixtures
import microversion_parse
import mock
from oslo_config import cfg
from oslo_middleware import request_id
from oslo_utils.fixture import uuidsentinel
from oslo_utils import timeutils
@ -33,9 +32,6 @@ from placement.objects import resource_provider as rp_obj
from placement import util
CONF = cfg.CONF
class TestCheckAccept(testtools.TestCase):
"""Confirm behavior of util.check_accept."""

View File

@ -11,13 +11,9 @@
# under the License.
"""Utility methods for placement API."""
import contextlib
import functools
import shutil
import tempfile
import jsonschema
from oslo_config import cfg
from oslo_log import log as logging
from oslo_middleware import request_id
from oslo_serialization import jsonutils
@ -31,7 +27,6 @@ from placement.i18n import _
# microversion
import placement.microversion
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
# Error code handling constants
@ -398,21 +393,6 @@ def normalize_member_of_qs_param(value):
return value
@contextlib.contextmanager
def tempdir(**kwargs):
argdict = kwargs.copy()
if 'dir' not in argdict:
argdict['dir'] = CONF.tempdir
tmpdir = tempfile.mkdtemp(**argdict)
try:
yield tmpdir
finally:
try:
shutil.rmtree(tmpdir)
except OSError as e:
LOG.error('Could not remove tmpdir: %s', e)
def run_once(message, logger, cleanup=None):
"""This is a utility function decorator to ensure a function
is run once and only once in an interpreter instance.

View File

@ -18,6 +18,7 @@ import logging as py_logging
import os
import os.path
from oslo_config import cfg
from oslo_log import log as logging
from oslo_middleware import cors
from oslo_policy import opts as policy_opts
@ -67,21 +68,23 @@ def _get_config_files(env=None):
return None
def _parse_args(argv, default_config_files):
logging.register_options(conf.CONF)
def _parse_args(config, argv, default_config_files):
# register placement's config options
conf.register_opts(config)
logging.register_options(config)
if profiler:
profiler.set_defaults(conf.CONF)
profiler.set_defaults(config)
_set_middleware_defaults()
# This is needed so we can check [oslo_policy]/enforce_scope in the
# deploy module.
policy_opts.set_defaults(conf.CONF)
policy_opts.set_defaults(config)
conf.CONF(argv[1:], project='placement',
version=version_info.version_string(),
default_config_files=default_config_files)
config(argv[1:], project='placement',
version=version_info.version_string(),
default_config_files=default_config_files)
def _set_middleware_defaults():
@ -110,26 +113,25 @@ def init_application():
# initialize the config system
conffiles = _get_config_files()
# NOTE(lyarwood): Call reset to ensure the ConfigOpts object doesn't
# already contain registered options if the app is reloaded.
conf.CONF.reset()
config = cfg.ConfigOpts()
conf.register_opts(config)
# This will raise cfg.RequiredOptError when a required option is not set
# (notably the database connection string). We want this to be a hard fail
# that prevents the application from starting. The error will show up in
# the wsgi server's logs.
_parse_args([], default_config_files=conffiles)
_parse_args(config, [], default_config_files=conffiles)
# initialize the logging system
setup_logging(conf.CONF)
setup_logging(config)
# configure database
db_api.configure(conf.CONF)
db_api.configure(config)
# dump conf at debug if log_options
if conf.CONF.log_options:
conf.CONF.log_opt_values(
if config.log_options:
config.log_opt_values(
logging.getLogger(__name__),
logging.DEBUG)
# build and return our WSGI app
return deploy.loadapp(conf.CONF)
return deploy.loadapp(config)