summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt Riedemann <mriedem.os@gmail.com>2017-11-30 18:09:00 -0500
committerMatt Riedemann <mriedem.os@gmail.com>2018-05-17 11:12:16 -0400
commit0a461979df62cd1df2c807b3f4fb3593b3040d13 (patch)
treec0f0b7ee06fcb7370285f992f77624b202d6c3f6
parentccc02de36c6b05c45400ff4ede9c6af4561cef7e (diff)
downloadnova-0a461979df62cd1df2c807b3f4fb3593b3040d13.tar.gz
Implement granular policy rules for placement
This adds a granular policy checking framework for placement based on nova.policy but with a lot of the legacy cruft removed, like the is_admin and context_is_admin rules. A new PlacementPolicyFixture is added along with a new configuration option, [placement]/policy_file, which is needed because the default policy file that gets used in config is from [oslo_policy]/policy_file which is being used as the nova policy file. As far as I can tell, oslo.policy doesn't allow for multiple policy files with different names unless I'm misunderstanding how the policy_dirs option works. With these changes, we can have something like: /etc/nova/policy.json - for nova policy rules /etc/nova/placement-policy.yaml - for placement rules The docs are also updated to include the placement policy sample along with a tox builder for the sample. This starts by adding granular rules for CRUD operations on the /resource_providers and /resource_providers/{uuid} routes which use the same descriptions from the placement API reference. Subsequent patches will add new granular rules for the other routes. Part of blueprint granular-placement-policy Change-Id: I17573f5210314341c332fdcb1ce462a989c21940
-rw-r--r--.gitignore1
-rw-r--r--doc/source/conf.py6
-rw-r--r--doc/source/configuration/index.rst22
-rw-r--r--doc/source/configuration/placement-policy.rst10
-rw-r--r--doc/source/configuration/policy.rst6
-rw-r--r--doc/source/configuration/sample-placement-policy.rst16
-rw-r--r--doc/source/configuration/sample-policy.rst6
-rw-r--r--etc/nova/README-policy.yaml.txt22
-rw-r--r--etc/nova/placement-policy-generator.conf5
-rw-r--r--lower-constraints.txt2
-rw-r--r--nova/api/openstack/placement/auth.py10
-rw-r--r--nova/api/openstack/placement/context.py52
-rw-r--r--nova/api/openstack/placement/exception.py4
-rw-r--r--nova/api/openstack/placement/handler.py51
-rw-r--r--nova/api/openstack/placement/handlers/resource_provider.py8
-rw-r--r--nova/api/openstack/placement/policies/__init__.py23
-rw-r--r--nova/api/openstack/placement/policies/base.py41
-rw-r--r--nova/api/openstack/placement/policies/resource_provider.py81
-rw-r--r--nova/api/openstack/placement/policy.py103
-rw-r--r--nova/conf/placement.py9
-rw-r--r--nova/hacking/checks.py11
-rw-r--r--nova/test.py2
-rw-r--r--nova/tests/functional/api/openstack/placement/fixtures.py27
-rw-r--r--nova/tests/functional/api/openstack/placement/gabbits/resource-provider-policy.yaml48
-rw-r--r--nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml9
-rw-r--r--nova/tests/unit/api/openstack/placement/test_context.py68
-rw-r--r--nova/tests/unit/api/openstack/placement/test_policy.py80
-rw-r--r--nova/tests/unit/policy_fixture.py30
-rw-r--r--nova/tests/unit/test_fixtures.py8
-rw-r--r--releasenotes/notes/bp-granular-placement-policy-65722fc6d7cb1359.yaml31
-rw-r--r--requirements.txt2
-rw-r--r--setup.cfg2
-rw-r--r--tox.ini3
33 files changed, 711 insertions, 88 deletions
diff --git a/.gitignore b/.gitignore
index 17e49b7b97..f254e05459 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,7 @@ nova/vcsversion.py
tools/conf/nova.conf*
doc/source/_static/nova.conf.sample
doc/source/_static/nova.policy.yaml.sample
+doc/source/_static/placement.policy.yaml.sample
# Files created by releasenotes build
releasenotes/build
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 61d4850405..78a8b0f6be 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -57,8 +57,10 @@ bug_tag = ''
config_generator_config_file = '../../etc/nova/nova-config-generator.conf'
sample_config_basename = '_static/nova'
-policy_generator_config_file = '../../etc/nova/nova-policy-generator.conf'
-sample_policy_basename = '_static/nova'
+policy_generator_config_file = [
+ ('../../etc/nova/nova-policy-generator.conf', '_static/nova'),
+ ('../../etc/nova/placement-policy-generator.conf', '_static/placement')
+]
actdiag_html_image_format = 'SVG'
actdiag_antialias = True
diff --git a/doc/source/configuration/index.rst b/doc/source/configuration/index.rst
index 66081dcbf1..7ebb6e72fd 100644
--- a/doc/source/configuration/index.rst
+++ b/doc/source/configuration/index.rst
@@ -20,8 +20,8 @@ Configuration
* :doc:`Sample Config File <sample-config>`: A sample config
file with inline documentation.
-Policy
-------
+Nova Policy
+-----------
Nova, like most OpenStack projects, uses a policy language to restrict
permissions on REST API actions.
@@ -29,8 +29,20 @@ permissions on REST API actions.
* :doc:`Policy Reference <policy>`: A complete reference of all
policy points in nova and what they impact.
-* :doc:`Sample Policy File <sample-policy>`: A sample policy
- file with inline documentation.
+* :doc:`Sample Policy File <sample-policy>`: A sample nova
+ policy file with inline documentation.
+
+Placement Policy
+----------------
+
+Placement, like most OpenStack projects, uses a policy language to restrict
+permissions on REST API actions.
+
+* :doc:`Policy Reference <placement-policy>`: A complete
+ reference of all policy points in placement and what they impact.
+
+* :doc:`Sample Policy File <sample-placement-policy>`: A sample
+ placement policy file with inline documentation.
.. # NOTE(mriedem): This is the section where we hide things that we don't
@@ -43,3 +55,5 @@ permissions on REST API actions.
sample-config
policy
sample-policy
+ placement-policy
+ sample-placement-policy
diff --git a/doc/source/configuration/placement-policy.rst b/doc/source/configuration/placement-policy.rst
new file mode 100644
index 0000000000..67b6cf6d55
--- /dev/null
+++ b/doc/source/configuration/placement-policy.rst
@@ -0,0 +1,10 @@
+==================
+Placement Policies
+==================
+
+The following is an overview of all available policies in Placement.
+For a sample configuration file, refer to
+:doc:`/configuration/sample-placement-policy`.
+
+.. show-policy::
+ :config-file: etc/nova/placement-policy-generator.conf
diff --git a/doc/source/configuration/policy.rst b/doc/source/configuration/policy.rst
index 8fea1406e5..66b4c7982d 100644
--- a/doc/source/configuration/policy.rst
+++ b/doc/source/configuration/policy.rst
@@ -1,6 +1,6 @@
-========
-Policies
-========
+=============
+Nova Policies
+=============
The following is an overview of all available policies in Nova. For a sample
configuration file, refer to :doc:`/configuration/sample-policy`.
diff --git a/doc/source/configuration/sample-placement-policy.rst b/doc/source/configuration/sample-placement-policy.rst
new file mode 100644
index 0000000000..12e21c52a4
--- /dev/null
+++ b/doc/source/configuration/sample-placement-policy.rst
@@ -0,0 +1,16 @@
+============================
+Sample Placement Policy File
+============================
+
+The following is a sample placement policy file for adaptation and use.
+
+The sample policy can also be viewed in :download:`file form
+</_static/placement.policy.yaml.sample>`.
+
+.. important::
+
+ The sample policy file is auto-generated from placement when this
+ documentation is built. You must ensure your version of placement matches
+ the version of this documentation.
+
+.. literalinclude:: /_static/placement.policy.yaml.sample
diff --git a/doc/source/configuration/sample-policy.rst b/doc/source/configuration/sample-policy.rst
index 0e4b699af7..1dc10e4749 100644
--- a/doc/source/configuration/sample-policy.rst
+++ b/doc/source/configuration/sample-policy.rst
@@ -1,6 +1,6 @@
-==================
-Sample Policy File
-==================
+=======================
+Sample Nova Policy File
+=======================
The following is a sample nova policy file for adaptation and use.
diff --git a/etc/nova/README-policy.yaml.txt b/etc/nova/README-policy.yaml.txt
index b4a233bc32..7599f80712 100644
--- a/etc/nova/README-policy.yaml.txt
+++ b/etc/nova/README-policy.yaml.txt
@@ -1,8 +1,24 @@
-To generate the sample policy.yaml file, run the following command from the top
-level of the nova directory:
+Nova
+====
+
+To generate the sample nova policy.yaml file, run the following command from
+the top level of the nova directory:
tox -egenpolicy
-For a pre-generated example of the latest policy.yaml, see:
+For a pre-generated example of the latest nova policy.yaml, see:
https://docs.openstack.org/nova/latest/configuration/sample-policy.html
+
+
+Placement
+=========
+
+To generate the sample placement policy.yaml file, run the following command
+from the top level of the nova directory:
+
+ tox -e genplacementpolicy
+
+For a pre-generated example of the latest placement policy.yaml, see:
+
+ https://docs.openstack.org/nova/latest/configuration/sample-placement-policy.html
diff --git a/etc/nova/placement-policy-generator.conf b/etc/nova/placement-policy-generator.conf
new file mode 100644
index 0000000000..a2e0697d00
--- /dev/null
+++ b/etc/nova/placement-policy-generator.conf
@@ -0,0 +1,5 @@
+[DEFAULT]
+# TODO: When placement is split out of the nova repo, this can change to
+# etc/placement/policy.yaml.sample.
+output_file = etc/nova/placement-policy.yaml.sample
+namespace = placement
diff --git a/lower-constraints.txt b/lower-constraints.txt
index a1e888e122..abec379138 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -83,7 +83,7 @@ oslo.i18n==3.15.3
oslo.log==3.36.0
oslo.messaging==5.29.0
oslo.middleware==3.31.0
-oslo.policy==1.30.0
+oslo.policy==1.35.0
oslo.privsep==1.23.0
oslo.reports==1.18.0
oslo.rootwrap==5.8.0
diff --git a/nova/api/openstack/placement/auth.py b/nova/api/openstack/placement/auth.py
index 0a072b5fb6..ff2551e26f 100644
--- a/nova/api/openstack/placement/auth.py
+++ b/nova/api/openstack/placement/auth.py
@@ -12,13 +12,12 @@
from keystonemiddleware import auth_token
-from oslo_context import context
-from oslo_db.sqlalchemy import enginefacade
from oslo_log import log as logging
from oslo_middleware import request_id
import webob.dec
import webob.exc
+from nova.api.openstack.placement import context
LOG = logging.getLogger(__name__)
@@ -57,11 +56,6 @@ class NoAuthMiddleware(Middleware):
return self.application
-@enginefacade.transaction_context_provider
-class RequestContext(context.RequestContext):
- pass
-
-
class PlacementKeystoneContext(Middleware):
"""Make a request context from keystone headers."""
@@ -69,7 +63,7 @@ class PlacementKeystoneContext(Middleware):
def __call__(self, req):
req_id = req.environ.get(request_id.ENV_REQUEST_ID)
- ctx = RequestContext.from_environ(
+ ctx = context.RequestContext.from_environ(
req.environ, request_id=req_id)
if ctx.user_id is None and req.environ['PATH_INFO'] != '/':
diff --git a/nova/api/openstack/placement/context.py b/nova/api/openstack/placement/context.py
new file mode 100644
index 0000000000..51b1340269
--- /dev/null
+++ b/nova/api/openstack/placement/context.py
@@ -0,0 +1,52 @@
+# 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.
+
+from oslo_context import context
+from oslo_db.sqlalchemy import enginefacade
+
+from nova.api.openstack.placement import exception
+from nova.api.openstack.placement import policy
+
+
+@enginefacade.transaction_context_provider
+class RequestContext(context.RequestContext):
+
+ def can(self, action, target=None, fatal=True):
+ """Verifies that the given action is valid on the target in this
+ context.
+
+ :param action: string representing the action to be checked.
+ :param target: As much information about the object being operated on
+ as possible. The target argument should be a dict instance or an
+ instance of a class that fully supports the Mapping abstract base
+ class and deep copying. For object creation this should be a
+ dictionary representing the location of the object e.g.
+ ``{'project_id': context.project_id}``. If None, then this default
+ target will be considered::
+
+ {'project_id': self.project_id, 'user_id': self.user_id}
+ :param fatal: if False, will return False when an
+ exception.PolicyNotAuthorized occurs.
+ :raises nova.exception.PolicyNotAuthorized: if verification fails and
+ fatal is True.
+ :return: returns a non-False value (not necessarily "True") if
+ authorized and False if not authorized and fatal is False.
+ """
+ if target is None:
+ target = {'project_id': self.project_id,
+ 'user_id': self.user_id}
+ try:
+ return policy.authorize(self, action, target)
+ except exception.PolicyNotAuthorized:
+ if fatal:
+ raise
+ return False
diff --git a/nova/api/openstack/placement/exception.py b/nova/api/openstack/placement/exception.py
index d5a4461ef1..208f8a6a8a 100644
--- a/nova/api/openstack/placement/exception.py
+++ b/nova/api/openstack/placement/exception.py
@@ -121,6 +121,10 @@ class ObjectActionError(_BaseException):
msg_fmt = _('Object action %(action)s failed because: %(reason)s')
+class PolicyNotAuthorized(_BaseException):
+ msg_fmt = _("Policy does not allow %(action)s to be performed.")
+
+
class ResourceClassCannotDeleteStandard(_BaseException):
msg_fmt = _("Cannot delete standard resource class %(resource_class)s.")
diff --git a/nova/api/openstack/placement/handler.py b/nova/api/openstack/placement/handler.py
index 18536bb133..1e36a9e0ec 100644
--- a/nova/api/openstack/placement/handler.py
+++ b/nova/api/openstack/placement/handler.py
@@ -23,10 +23,13 @@ Routes.Mapper, including automatic handlers to respond with a
method.
"""
+import re
+
import routes
import webob
from oslo_log import log as logging
+from oslo_utils import excutils
from nova.api.openstack.placement import exception
from nova.api.openstack.placement.handlers import aggregate
@@ -38,7 +41,6 @@ from nova.api.openstack.placement.handlers import resource_provider
from nova.api.openstack.placement.handlers import root
from nova.api.openstack.placement.handlers import trait
from nova.api.openstack.placement.handlers import usage
-from nova.api.openstack.placement import policy
from nova.api.openstack.placement import util
from nova.i18n import _
@@ -129,6 +131,19 @@ ROUTE_DECLARATIONS = {
},
}
+# This is a temporary list (of regexes) of the route handlers that will do
+# their own granular policy check. Once all handlers are doing their own
+# policy checks we can remove this along with the generic policy check in
+# PlacementHandler. All entries are checked against re.match() so must
+# match the start of the path.
+PER_ROUTE_POLICY = [
+ # The root is special in that it does not require auth.
+ '/$',
+ # /resource_providers
+ # /resource_providers/{uuid}
+ '/resource_providers(/[A-Za-z0-9-]+)?$'
+]
+
def dispatch(environ, start_response, mapper):
"""Find a matching route for the current request.
@@ -192,17 +207,29 @@ class PlacementHandler(object):
# NOTE(cdent): Local config currently unused.
self._map = make_map(ROUTE_DECLARATIONS)
+ @staticmethod
+ def _is_granular_policy_check(path):
+ for policy in PER_ROUTE_POLICY:
+ if re.match(policy, path):
+ return True
+ return False
+
def __call__(self, environ, start_response):
- # All requests but '/' require admin.
- if environ['PATH_INFO'] != '/':
+ # Any routes that do not yet have a granular policy check default
+ # to admin-only.
+ if not self._is_granular_policy_check(environ['PATH_INFO']):
context = environ['placement.context']
- # TODO(cdent): Using is_admin everywhere (except /) is
- # insufficiently flexible for future use case but is
- # convenient for initial exploration.
- if not policy.placement_authorize(context, 'placement'):
- raise webob.exc.HTTPForbidden(
- _('admin required'),
- json_formatter=util.json_error_formatter)
+ try:
+ if not context.can('placement', fatal=False):
+ raise webob.exc.HTTPForbidden(
+ _('admin required'),
+ json_formatter=util.json_error_formatter)
+ except Exception:
+ # This is here mostly for help in debugging problems with
+ # busted test setup.
+ with excutils.save_and_reraise_exception():
+ LOG.exception('policy check failed for path: %s',
+ environ['PATH_INFO'])
# 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.
@@ -223,6 +250,10 @@ class PlacementHandler(object):
except exception.NotFound as exc:
raise webob.exc.HTTPNotFound(
exc, json_formatter=util.json_error_formatter)
+ except exception.PolicyNotAuthorized as exc:
+ raise webob.exc.HTTPForbidden(
+ exc.format_message(),
+ json_formatter=util.json_error_formatter)
# Remaining uncaught exceptions will rise first to the Microversion
# middleware, where any WebOb generated exceptions will be caught and
# transformed into legit HTTP error responses (with microversion
diff --git a/nova/api/openstack/placement/handlers/resource_provider.py b/nova/api/openstack/placement/handlers/resource_provider.py
index c80e91fd82..86a83812e6 100644
--- a/nova/api/openstack/placement/handlers/resource_provider.py
+++ b/nova/api/openstack/placement/handlers/resource_provider.py
@@ -21,6 +21,7 @@ import webob
from nova.api.openstack.placement import exception
from nova.api.openstack.placement import microversion
from nova.api.openstack.placement.objects import resource_provider as rp_obj
+from nova.api.openstack.placement.policies import resource_provider as policies
from nova.api.openstack.placement.schemas import resource_provider as rp_schema
from nova.api.openstack.placement import util
from nova.api.openstack.placement import wsgi_wrapper
@@ -78,6 +79,7 @@ def create_resource_provider(req):
header pointing to the newly created resource provider.
"""
context = req.environ['placement.context']
+ context.can(policies.CREATE)
schema = rp_schema.POST_RESOURCE_PROVIDER_SCHEMA
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
if want_version.matches((1, 14)):
@@ -126,6 +128,7 @@ def delete_resource_provider(req):
"""
uuid = util.wsgi_path_item(req.environ, 'uuid')
context = req.environ['placement.context']
+ context.can(policies.DELETE)
# The containing application will catch a not found here.
try:
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
@@ -153,9 +156,10 @@ def get_resource_provider(req):
"""
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
uuid = util.wsgi_path_item(req.environ, 'uuid')
- # The containing application will catch a not found here.
context = req.environ['placement.context']
+ context.can(policies.SHOW)
+ # The containing application will catch a not found here.
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
context, uuid)
@@ -179,6 +183,7 @@ def list_resource_providers(req):
a collection of resource providers.
"""
context = req.environ['placement.context']
+ context.can(policies.LIST)
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
schema = rp_schema.GET_RPS_SCHEMA_1_0
@@ -244,6 +249,7 @@ def update_resource_provider(req):
"""
uuid = util.wsgi_path_item(req.environ, 'uuid')
context = req.environ['placement.context']
+ context.can(policies.UPDATE)
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
# The containing application will catch a not found here.
diff --git a/nova/api/openstack/placement/policies/__init__.py b/nova/api/openstack/placement/policies/__init__.py
new file mode 100644
index 0000000000..575f1d8642
--- /dev/null
+++ b/nova/api/openstack/placement/policies/__init__.py
@@ -0,0 +1,23 @@
+# 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.
+
+import itertools
+
+from nova.api.openstack.placement.policies import base
+from nova.api.openstack.placement.policies import resource_provider
+
+
+def list_rules():
+ return itertools.chain(
+ base.list_rules(),
+ resource_provider.list_rules(),
+ )
diff --git a/nova/api/openstack/placement/policies/base.py b/nova/api/openstack/placement/policies/base.py
new file mode 100644
index 0000000000..f196994d80
--- /dev/null
+++ b/nova/api/openstack/placement/policies/base.py
@@ -0,0 +1,41 @@
+# 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.
+
+from oslo_policy import policy
+
+RULE_ADMIN_API = 'rule:admin_api'
+
+rules = [
+ # "placement" is the default rule (action) used for all routes that do
+ # not yet have granular policy rules. It is used in
+ # PlacementHandler.__call__ and can be dropped once all routes have
+ # granular policy handling.
+ policy.RuleDefault(
+ "placement",
+ "role:admin",
+ description="This rule is used for all routes that do not yet "
+ "have granular policy rules. It will be replaced "
+ "with rule:admin_api.",
+ deprecated_for_removal=True,
+ deprecated_reason="This was a catch-all rule hard-coded into "
+ "the placement service and has been superseded by "
+ "granular policy rules per operation.",
+ deprecated_since="18.0.0"),
+ policy.RuleDefault(
+ "admin_api",
+ "role:admin",
+ description="Default rule for most placement APIs."),
+]
+
+
+def list_rules():
+ return rules
diff --git a/nova/api/openstack/placement/policies/resource_provider.py b/nova/api/openstack/placement/policies/resource_provider.py
new file mode 100644
index 0000000000..027985c204
--- /dev/null
+++ b/nova/api/openstack/placement/policies/resource_provider.py
@@ -0,0 +1,81 @@
+# 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.
+
+
+from oslo_policy import policy
+
+from nova.api.openstack.placement.policies import base
+
+
+PREFIX = 'placement:resource_providers:%s'
+LIST = PREFIX % 'list'
+CREATE = PREFIX % 'create'
+SHOW = PREFIX % 'show'
+UPDATE = PREFIX % 'update'
+DELETE = PREFIX % 'delete'
+
+rules = [
+ policy.DocumentedRuleDefault(
+ LIST,
+ base.RULE_ADMIN_API,
+ "List resource providers.",
+ [
+ {
+ 'method': 'GET',
+ 'path': '/resource_providers'
+ }
+ ]),
+ policy.DocumentedRuleDefault(
+ CREATE,
+ base.RULE_ADMIN_API,
+ "Create resource provider.",
+ [
+ {
+ 'method': 'POST',
+ 'path': '/resource_providers'
+ }
+ ]),
+ policy.DocumentedRuleDefault(
+ SHOW,
+ base.RULE_ADMIN_API,
+ "Show resource provider.",
+ [
+ {
+ 'method': 'GET',
+ 'path': '/resource_providers/{uuid}'
+ }
+ ]),
+ policy.DocumentedRuleDefault(
+ UPDATE,
+ base.RULE_ADMIN_API,
+ "Update resource provider.",
+ [
+ {
+ 'method': 'PUT',
+ 'path': '/resource_providers/{uuid}'
+ }
+ ]),
+ policy.DocumentedRuleDefault(
+ DELETE,
+ base.RULE_ADMIN_API,
+ "Delete resource provider.",
+ [
+ {
+ 'method': 'DELETE',
+ 'path': '/resource_providers/{uuid}'
+ }
+ ]),
+]
+
+
+def list_rules():
+ return rules
diff --git a/nova/api/openstack/placement/policy.py b/nova/api/openstack/placement/policy.py
index fd856635b6..a280b484c4 100644
--- a/nova/api/openstack/placement/policy.py
+++ b/nova/api/openstack/placement/policy.py
@@ -14,7 +14,10 @@
from oslo_config import cfg
from oslo_log import log as logging
from oslo_policy import policy
-from oslo_serialization import jsonutils
+from oslo_utils import excutils
+
+from nova.api.openstack.placement import exception
+from nova.api.openstack.placement import policies
CONF = cfg.CONF
@@ -22,54 +25,68 @@ LOG = logging.getLogger(__name__)
_ENFORCER_PLACEMENT = None
-def placement_init():
- """Init an Enforcer class for placement policy.
+def reset():
+ """Used to reset the global _ENFORCER_PLACEMENT between test runs."""
+ global _ENFORCER_PLACEMENT
+ if _ENFORCER_PLACEMENT:
+ _ENFORCER_PLACEMENT.clear()
+ _ENFORCER_PLACEMENT = None
+
- This method uses a different list of policies than other parts of Nova.
- This is done to facilitate a split out of the placement service later.
- """
+def init():
+ """Init an Enforcer class. Sets the _ENFORCER_PLACEMENT global."""
global _ENFORCER_PLACEMENT
if not _ENFORCER_PLACEMENT:
- # TODO(cdent): Using is_admin everywhere (except /) is
- # insufficiently flexible for future use case but is
- # convenient for initial exploration. We will need to
- # determine how to manage authorization/policy and
- # implement that, probably per handler.
- rules = policy.Rules.load(jsonutils.dumps({'placement': 'role:admin'}))
- # Enforcer is initialized so that the above rule is loaded in and no
- # policy file is read.
- # TODO(alaski): Register a default rule rather than loading it in like
- # this. That requires that a policy file is specified to be read. When
- # this is split out such that a placement policy file makes sense then
- # change to rule registration.
- _ENFORCER_PLACEMENT = policy.Enforcer(CONF, rules=rules,
- use_conf=False)
+ # 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 placement_authorize(context, action, target=None):
- """Verifies that the action is valid on the target in this context.
+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='nova')
+ init()
+ return _ENFORCER_PLACEMENT
+
- :param context: RequestContext object
- :param action: string representing the action to be checked
- :param target: dictionary representing the object of the action
- for object creation this should be a dictionary representing the
- location of the object e.g. ``{'project_id': context.project_id}``
+def authorize(context, action, target, do_raise=True):
+ """Verifies that the action is valid on the target in this context.
- :return: returns a non-False value (not necessarily "True") if
- authorized, and the exact value False if not authorized.
+ :param context: instance of
+ nova.api.openstack.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 nova.api.openstack.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.
"""
- placement_init()
- if target is None:
- target = {'project_id': context.project_id,
- 'user_id': context.user_id}
+ init()
credentials = context.to_policy_values()
- # TODO(alaski): Change this to use authorize() when rules are registered.
- # noqa the following line because a hacking check disallows using enforce.
- result = _ENFORCER_PLACEMENT.enforce(action, target, credentials,
- do_raise=False, exc=None,
- action=action)
- if result is False:
- LOG.debug('Policy check for %(action)s failed with credentials '
- '%(credentials)s',
- {'action': action, 'credentials': credentials})
- return result
+ 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})
diff --git a/nova/conf/placement.py b/nova/conf/placement.py
index 9dd5459155..fd68dc13d1 100644
--- a/nova/conf/placement.py
+++ b/nova/conf/placement.py
@@ -35,6 +35,15 @@ being equal, two requests for allocation candidates will return the same
results in the same order; but no guarantees are made as to how that order
is determined.
"""),
+ # TODO(mriedem): When placement is split out of nova, this should be
+ # deprecated since then [oslo_policy]/policy_file can be used.
+ cfg.StrOpt(
+ 'policy_file',
+ # This default matches what is in
+ # etc/nova/placement-policy-generator.conf
+ default='placement-policy.yaml',
+ help='The file that defines placement policies. This can be an '
+ 'absolute path or relative to the configuration file.'),
]
diff --git a/nova/hacking/checks.py b/nova/hacking/checks.py
index eea0a775e6..3780f89761 100644
--- a/nova/hacking/checks.py
+++ b/nova/hacking/checks.py
@@ -621,13 +621,16 @@ def check_config_option_in_central_place(logical_line, filename):
def check_policy_registration_in_central_place(logical_line, filename):
- msg = ('N350: Policy registration should be in the central location '
- '"/nova/policies/*".')
+ msg = ('N350: Policy registration should be in the central location(s) '
+ '"/nova/policies/*" or "nova/api/openstack/placement/policies/*".')
# This is where registration should happen
- if "nova/policies/" in filename:
+ if ("nova/policies/" in filename or
+ "nova/api/openstack/placement/policies/" in filename):
return
# A couple of policy tests register rules
- if "nova/tests/unit/test_policy.py" in filename:
+ if ("nova/tests/unit/test_policy.py" in filename or
+ "nova/tests/unit/api/openstack/placement/test_policy.py" in
+ filename):
return
if rule_default_re.match(logical_line):
diff --git a/nova/test.py b/nova/test.py
index a22025f488..d7917f5e16 100644
--- a/nova/test.py
+++ b/nova/test.py
@@ -322,6 +322,8 @@ class TestCase(testtools.TestCase):
self.addCleanup(self._clear_attrs)
self.useFixture(fixtures.EnvironmentVariable('http_proxy'))
self.policy = self.useFixture(policy_fixture.PolicyFixture())
+ self.placement_policy = self.useFixture(
+ policy_fixture.PlacementPolicyFixture())
self.useFixture(nova_fixtures.PoisonFunctions())
diff --git a/nova/tests/functional/api/openstack/placement/fixtures.py b/nova/tests/functional/api/openstack/placement/fixtures.py
index 31c898d114..c84f8f6b86 100644
--- a/nova/tests/functional/api/openstack/placement/fixtures.py
+++ b/nova/tests/functional/api/openstack/placement/fixtures.py
@@ -19,10 +19,12 @@ from oslo_utils import uuidutils
from nova.api.openstack.placement import deploy
from nova.api.openstack.placement import exception
from nova.api.openstack.placement.objects import resource_provider as rp_obj
+from nova.api.openstack.placement import policies
from nova import conf
from nova import config
from nova import context
from nova.tests import fixtures
+from nova.tests.unit import policy_fixture
from nova.tests import uuidsentinel as uuids
@@ -514,3 +516,28 @@ class GranularFixture(APIFixture):
_add_inventory(shr_net, 'SRIOV_NET_VF', 16)
_add_inventory(shr_net, 'CUSTOM_NET_MBPS', 40000)
_set_traits(shr_net, 'MISC_SHARES_VIA_AGGREGATE')
+
+
+class OpenPolicyFixture(APIFixture):
+ """An APIFixture that changes all policy rules to allow non-admins."""
+
+ def start_fixture(self):
+ super(OpenPolicyFixture, self).start_fixture()
+ self.placement_policy_fixture = policy_fixture.PlacementPolicyFixture()
+ 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
+ # have a user_id/project_id attribute.
+ rules = {}
+ for rule in policies.list_rules():
+ name = rule.name
+ # Ignore "base" rules for role:admin.
+ if name in ['placement', 'admin_api']:
+ continue
+ rules[name] = '@'
+ self.placement_policy_fixture.set_rules(rules)
+
+ def stop_fixture(self):
+ super(OpenPolicyFixture, self).stop_fixture()
+ self.placement_policy_fixture.cleanUp()
diff --git a/nova/tests/functional/api/openstack/placement/gabbits/resource-provider-policy.yaml b/nova/tests/functional/api/openstack/placement/gabbits/resource-provider-policy.yaml
new file mode 100644
index 0000000000..ef663f1b48
--- /dev/null
+++ b/nova/tests/functional/api/openstack/placement/gabbits/resource-provider-policy.yaml
@@ -0,0 +1,48 @@
+# This tests the individual CRUD operations on /resource_providers
+# using a non-admin user with an open policy configuration. The
+# response validation is intentionally minimal.
+fixtures:
+ - OpenPolicyFixture
+
+defaults:
+ request_headers:
+ x-auth-token: user
+ accept: application/json
+ content-type: application/json
+ openstack-api-version: placement latest
+
+tests:
+
+- name: list resource providers
+ GET: /resource_providers
+ response_json_paths:
+ $.resource_providers: []
+
+- name: create resource provider
+ POST: /resource_providers
+ request_headers:
+ content-type: application/json
+ data:
+ name: $ENVIRON['RP_NAME']
+ uuid: $ENVIRON['RP_UUID']
+ status: 200
+ response_json_paths:
+ $.uuid: $ENVIRON['RP_UUID']
+
+- name: show resource provider
+ GET: /resource_providers/$ENVIRON['RP_UUID']
+ response_json_paths:
+ $.uuid: $ENVIRON['RP_UUID']
+
+- name: update resource provider
+ PUT: /resource_providers/$ENVIRON['RP_UUID']
+ data:
+ name: new name
+ status: 200
+ response_json_paths:
+ $.name: new name
+ $.uuid: $ENVIRON['RP_UUID']
+
+- name: delete resource provider
+ DELETE: /resource_providers/$ENVIRON['RP_UUID']
+ status: 204
diff --git a/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml b/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml
index b39ce63201..e86ba80059 100644
--- a/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml
+++ b/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml
@@ -31,14 +31,13 @@ tests:
response_json_paths:
$.errors[0].title: Forbidden
-- name: non admin forbidden non json
- GET: /resource_providers
+- name: route not found non json
+ GET: /moo
request_headers:
- x-auth-token: user
accept: text/plain
- status: 403
+ status: 404
response_strings:
- - admin required
+ - The resource could not be found
- name: post new resource provider - old microversion
POST: /resource_providers
diff --git a/nova/tests/unit/api/openstack/placement/test_context.py b/nova/tests/unit/api/openstack/placement/test_context.py
new file mode 100644
index 0000000000..1810161591
--- /dev/null
+++ b/nova/tests/unit/api/openstack/placement/test_context.py
@@ -0,0 +1,68 @@
+# 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.
+
+import mock
+import testtools
+
+from nova.api.openstack.placement import context
+from nova.api.openstack.placement import exception
+
+
+class TestPlacementRequestContext(testtools.TestCase):
+ """Test cases for PlacementRequestContext."""
+
+ def setUp(self):
+ super(TestPlacementRequestContext, self).setUp()
+ self.ctxt = context.RequestContext(user_id='fake', project_id='fake')
+ self.default_target = {'user_id': self.ctxt.user_id,
+ 'project_id': self.ctxt.project_id}
+
+ @mock.patch('nova.api.openstack.placement.policy.authorize',
+ return_value=True)
+ def test_can_target_none_fatal_true_accept(self, mock_authorize):
+ self.assertTrue(self.ctxt.can('placement:resource_providers:list'))
+ mock_authorize.assert_called_once_with(
+ self.ctxt, 'placement:resource_providers:list',
+ self.default_target)
+
+ @mock.patch('nova.api.openstack.placement.policy.authorize',
+ side_effect=exception.PolicyNotAuthorized(
+ action='placement:resource_providers:list'))
+ def test_can_target_none_fatal_true_reject(self, mock_authorize):
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.ctxt.can, 'placement:resource_providers:list')
+ mock_authorize.assert_called_once_with(
+ self.ctxt, 'placement:resource_providers:list',
+ self.default_target)
+
+ @mock.patch('nova.api.openstack.placement.policy.authorize',
+ side_effect=exception.PolicyNotAuthorized(
+ action='placement:resource_providers:list'))
+ def test_can_target_none_fatal_false_reject(self, mock_authorize):
+ self.assertFalse(self.ctxt.can('placement:resource_providers:list',
+ fatal=False))
+ mock_authorize.assert_called_once_with(
+ self.ctxt, 'placement:resource_providers:list',
+ self.default_target)
+
+ @mock.patch('nova.api.openstack.placement.policy.authorize',
+ return_value=True)
+ def test_can_target_none_fatal_true_accept_custom_target(
+ self, mock_authorize):
+ class MyObj(object):
+ user_id = project_id = 'fake2'
+
+ target = MyObj()
+ self.assertTrue(self.ctxt.can('placement:resource_providers:list',
+ target=target))
+ mock_authorize.assert_called_once_with(
+ self.ctxt, 'placement:resource_providers:list', target)
diff --git a/nova/tests/unit/api/openstack/placement/test_policy.py b/nova/tests/unit/api/openstack/placement/test_policy.py
new file mode 100644
index 0000000000..ad0bdf637a
--- /dev/null
+++ b/nova/tests/unit/api/openstack/placement/test_policy.py
@@ -0,0 +1,80 @@
+# 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.
+
+import os
+
+from oslo_policy import policy as oslo_policy
+import testtools
+
+from nova.api.openstack.placement import context
+from nova.api.openstack.placement import exception
+from nova.api.openstack.placement import policy
+from nova.tests.unit import conf_fixture
+from nova.tests.unit import policy_fixture
+from nova import utils
+
+
+class PlacementPolicyTestCase(testtools.TestCase):
+ """Tests interactions with placement policy.
+
+ These tests do not rely on the base nova.test.TestCase to avoid
+ interference from the PlacementPolicyFixture which is not used in all
+ test cases.
+ """
+ def setUp(self):
+ super(PlacementPolicyTestCase, self).setUp()
+ self.conf = self.useFixture(conf_fixture.ConfFixture()).conf
+ self.ctxt = context.RequestContext(user_id='fake', project_id='fake')
+ self.target = {'user_id': 'fake', 'project_id': 'fake'}
+
+ def test_modified_policy_reloads(self):
+ """Creates a temporary placement-policy.yaml file and tests
+ authorizations against a fake rule between updates to the physical
+ policy file.
+ """
+ with utils.tempdir() as tmpdir:
+ tmpfilename = os.path.join(tmpdir, 'placement-policy.yaml')
+
+ self.conf.set_default(
+ 'policy_file', tmpfilename, group='placement')
+
+ 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)
+
+ # 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)
+
+ def test_authorize_do_raise_false(self):
+ """Tests that authorize does not raise an exception when the check
+ fails.
+ """
+ fixture = self.useFixture(policy_fixture.PlacementPolicyFixture())
+ fixture.set_rules({'placement': '!'})
+ self.assertFalse(
+ policy.authorize(
+ self.ctxt, 'placement', self.target, do_raise=False))
diff --git a/nova/tests/unit/policy_fixture.py b/nova/tests/unit/policy_fixture.py
index a076afa93d..651f096bcb 100644
--- a/nova/tests/unit/policy_fixture.py
+++ b/nova/tests/unit/policy_fixture.py
@@ -18,6 +18,7 @@ import fixtures
from oslo_policy import policy as oslo_policy
from oslo_serialization import jsonutils
+from nova.api.openstack.placement import policy as placement_policy
import nova.conf
from nova.conf import paths
from nova import policies
@@ -126,3 +127,32 @@ class RoleBasedPolicyFixture(RealPolicyFixture):
self.policy_file = os.path.join(self.policy_dir.path, 'policy.json')
with open(self.policy_file, 'w') as f:
jsonutils.dump(policy, f)
+
+
+class PlacementPolicyFixture(fixtures.Fixture):
+ """Load the default placement policy for tests.
+
+ This fixture requires nova.tests.unit.conf_fixture.ConfFixture.
+ """
+ def setUp(self):
+ super(PlacementPolicyFixture, self).setUp()
+ policy_file = paths.state_path_def('etc/nova/placement-policy.yaml')
+ CONF.set_override('policy_file', policy_file, group='placement')
+ placement_policy.reset()
+ placement_policy.init()
+ self.addCleanup(placement_policy.reset)
+
+ @staticmethod
+ def set_rules(rules, overwrite=True):
+ """Set placement policy rules.
+
+ .. note:: The rules must first be registered via the
+ Enforcer.register_defaults method.
+
+ :param rules: dict of action=rule mappings to set
+ :param overwrite: Whether to overwrite current rules or update them
+ with the new rules.
+ """
+ enforcer = placement_policy.get_enforcer()
+ enforcer.set_rules(oslo_policy.Rules.from_dict(rules),
+ overwrite=overwrite)
diff --git a/nova/tests/unit/test_fixtures.py b/nova/tests/unit/test_fixtures.py
index cb5a4ceb1d..4e8b3e6f71 100644
--- a/nova/tests/unit/test_fixtures.py
+++ b/nova/tests/unit/test_fixtures.py
@@ -34,6 +34,7 @@ from nova.objects import base as obj_base
from nova.objects import service as service_obj
from nova.tests import fixtures
from nova.tests.unit import conf_fixture
+from nova.tests.unit import policy_fixture
from nova import utils
CONF = cfg.CONF
@@ -471,6 +472,13 @@ class TestSingleCellSimpleFixture(testtools.TestCase):
class TestPlacementFixture(testtools.TestCase):
+ def setUp(self):
+ super(TestPlacementFixture, self).setUp()
+ # We need ConfFixture since PlacementPolicyFixture reads from config.
+ self.useFixture(conf_fixture.ConfFixture())
+ # We need PlacementPolicyFixture because placement-api checks policy.
+ self.useFixture(policy_fixture.PlacementPolicyFixture())
+
def test_responds_to_version(self):
"""Ensure the Placement server responds to calls sensibly."""
placement_fixture = self.useFixture(fixtures.PlacementFixture())
diff --git a/releasenotes/notes/bp-granular-placement-policy-65722fc6d7cb1359.yaml b/releasenotes/notes/bp-granular-placement-policy-65722fc6d7cb1359.yaml
new file mode 100644
index 0000000000..90fc54a9e5
--- /dev/null
+++ b/releasenotes/notes/bp-granular-placement-policy-65722fc6d7cb1359.yaml
@@ -0,0 +1,31 @@
+---
+features:
+ - |
+ It is now possible to configure granular policy rules for placement
+ REST API operations.
+
+ By default, all operations continue to use the ``role:admin`` check string
+ so there is no upgrade impact.
+
+ A new configuration option is introduced, ``[placement]/policy_file``,
+ which is used to configure the location of the placement policy file.
+ By default, the ``placement-policy.yaml`` file may live alongside the
+ nova policy file, e.g.:
+
+ * /etc/nova/policy.yaml
+ * /etc/nova/placement-policy.yaml
+
+ However, if desired, ``[placement]/policy_file`` makes it possible to
+ package and deploy the placement policy file separately to make the future
+ split of placement and nova packages easier, e.g.:
+
+ * /etc/placement/policy.yaml
+
+ All placement policy rules are defined in code so by default no extra
+ configuration is required and the default rules will be used on start of
+ the placement service.
+
+ For more information about placement policy including a sample file, see
+ the configuration reference documentation:
+
+ https://docs.openstack.org/nova/latest/configuration/index.html#placement-policy
diff --git a/requirements.txt b/requirements.txt
index 1b7631771e..2ea7cbe67f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -44,7 +44,7 @@ oslo.utils>=3.33.0 # Apache-2.0
oslo.db>=4.27.0 # Apache-2.0
oslo.rootwrap>=5.8.0 # Apache-2.0
oslo.messaging>=5.29.0 # Apache-2.0
-oslo.policy>=1.30.0 # Apache-2.0
+oslo.policy>=1.35.0 # Apache-2.0
oslo.privsep>=1.23.0 # Apache-2.0
oslo.i18n>=3.15.3 # Apache-2.0
oslo.service!=1.28.1,>=1.24.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index 12f1286dd7..d2e073153c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -40,6 +40,7 @@ oslo.config.opts.defaults =
oslo.policy.enforcer =
nova = nova.policy:get_enforcer
+ placement = nova.api.openstack.placement.policy:get_enforcer
oslo.policy.policies =
# The sample policies will be ordered by entry point and then by list
@@ -47,6 +48,7 @@ oslo.policy.policies =
# list_rules method into a separate entry point rather than using the
# aggregate method.
nova = nova.policies:list_rules
+ placement = nova.api.openstack.placement.policies:list_rules
nova.compute.monitors.cpu =
virt_driver = nova.compute.monitors.cpu.virt_driver:Monitor
diff --git a/tox.ini b/tox.ini
index 8a3d2d8af3..c10d07f9e0 100644
--- a/tox.ini
+++ b/tox.ini
@@ -116,6 +116,9 @@ commands = oslo-config-generator --config-file=etc/nova/nova-config-generator.co
[testenv:genpolicy]
commands = oslopolicy-sample-generator --config-file=etc/nova/nova-policy-generator.conf
+[testenv:genplacementpolicy]
+commands = oslopolicy-sample-generator --config-file=etc/nova/placement-policy-generator.conf
+
[testenv:cover]
# Also do not run test_coverage_ext tests while gathering coverage as those
# tests conflict with coverage.