Check policy when handling a HTTP request
* Add default policy for handling the create request. * Allow it to be accessed only by nova service. * Remove unused code copied from cinder. Change-Id: Ieaa407f27c6774d1fd17850a9571de5554360bae
This commit is contained in:
parent
b3f961e331
commit
462305315c
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
build
|
build
|
||||||
files/join.conf
|
files/join.conf
|
||||||
|
files/policy.yaml.sample
|
||||||
tools/lintstack.head.py
|
tools/lintstack.head.py
|
||||||
*.pyc
|
*.pyc
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
3
files/policy-generator.conf
Normal file
3
files/policy-generator.conf
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
output_file = files/policy.yaml.sample
|
||||||
|
namespace = novajoin
|
@ -22,8 +22,6 @@ import copy
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_context import context
|
from oslo_context import context
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import timeutils
|
|
||||||
import six
|
|
||||||
|
|
||||||
from novajoin import policy
|
from novajoin import policy
|
||||||
|
|
||||||
@ -39,54 +37,9 @@ class RequestContext(context.RequestContext):
|
|||||||
Represents the user taking a given action within the system.
|
Represents the user taking a given action within the system.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, user_id, project_id, is_admin=None, read_deleted="no",
|
def __init__(self, *args, **kwargs):
|
||||||
roles=None, project_name=None, remote_address=None,
|
|
||||||
timestamp=None, request_id=None, auth_token=None,
|
|
||||||
overwrite=True, quota_class=None, service_catalog=None,
|
|
||||||
domain=None, user_domain=None, project_domain=None,
|
|
||||||
**kwargs):
|
|
||||||
"""Initialize RequestContext.
|
|
||||||
|
|
||||||
:param read_deleted: 'no' indicates deleted records are hidden, 'yes'
|
super(RequestContext, self).__init__(*args, **kwargs)
|
||||||
indicates deleted records are visible, 'only' indicates that
|
|
||||||
*only* deleted records are visible.
|
|
||||||
|
|
||||||
:param overwrite: Set to False to ensure that the greenthread local
|
|
||||||
copy of the index is not overwritten.
|
|
||||||
|
|
||||||
:param kwargs: Extra arguments that might be present, but we ignore
|
|
||||||
because they possibly came in from older rpc messages.
|
|
||||||
"""
|
|
||||||
|
|
||||||
super(RequestContext, self).__init__(auth_token=auth_token,
|
|
||||||
user=user_id,
|
|
||||||
tenant=project_id,
|
|
||||||
domain=domain,
|
|
||||||
user_domain=user_domain,
|
|
||||||
project_domain=project_domain,
|
|
||||||
is_admin=is_admin,
|
|
||||||
request_id=request_id,
|
|
||||||
overwrite=overwrite,
|
|
||||||
roles=roles)
|
|
||||||
self.project_name = project_name
|
|
||||||
self.read_deleted = read_deleted
|
|
||||||
self.remote_address = remote_address
|
|
||||||
if not timestamp:
|
|
||||||
timestamp = timeutils.utcnow()
|
|
||||||
elif isinstance(timestamp, six.string_types):
|
|
||||||
timestamp = timeutils.parse_isotime(timestamp)
|
|
||||||
self.timestamp = timestamp
|
|
||||||
self.quota_class = quota_class
|
|
||||||
|
|
||||||
if service_catalog:
|
|
||||||
# Only include required parts of service_catalog
|
|
||||||
self.service_catalog = [s for s in service_catalog
|
|
||||||
if s.get('type') in
|
|
||||||
('identity', 'compute', 'object-store',
|
|
||||||
'image')]
|
|
||||||
else:
|
|
||||||
# if list is empty or none
|
|
||||||
self.service_catalog = []
|
|
||||||
|
|
||||||
# We need to have RequestContext attributes defined
|
# We need to have RequestContext attributes defined
|
||||||
# when policy.check_is_admin invokes request logging
|
# when policy.check_is_admin invokes request logging
|
||||||
@ -96,39 +49,14 @@ class RequestContext(context.RequestContext):
|
|||||||
elif self.is_admin and 'admin' not in self.roles:
|
elif self.is_admin and 'admin' not in self.roles:
|
||||||
self.roles.append('admin')
|
self.roles.append('admin')
|
||||||
|
|
||||||
def _get_read_deleted(self):
|
|
||||||
return self._read_deleted
|
|
||||||
|
|
||||||
def _set_read_deleted(self, read_deleted):
|
|
||||||
if read_deleted not in ('no', 'yes', 'only'):
|
|
||||||
raise ValueError("read_deleted can only be one of 'no', "
|
|
||||||
"'yes' or 'only', not %r") % read_deleted
|
|
||||||
self._read_deleted = read_deleted
|
|
||||||
|
|
||||||
def _del_read_deleted(self):
|
|
||||||
del self._read_deleted
|
|
||||||
|
|
||||||
read_deleted = property(_get_read_deleted, _set_read_deleted,
|
|
||||||
_del_read_deleted)
|
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
result = super(RequestContext, self).to_dict()
|
result = super(RequestContext, self).to_dict()
|
||||||
result['user_id'] = self.user_id
|
result['user_id'] = self.user_id
|
||||||
|
result['user_name'] = self.user_name
|
||||||
result['project_id'] = self.project_id
|
result['project_id'] = self.project_id
|
||||||
result['project_name'] = self.project_name
|
result['project_name'] = self.project_name
|
||||||
result['domain'] = self.domain
|
|
||||||
result['read_deleted'] = self.read_deleted
|
|
||||||
result['remote_address'] = self.remote_address
|
|
||||||
result['timestamp'] = self.timestamp.isoformat()
|
|
||||||
result['quota_class'] = self.quota_class
|
|
||||||
result['service_catalog'] = self.service_catalog
|
|
||||||
result['request_id'] = self.request_id
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, values):
|
|
||||||
return cls(**values)
|
|
||||||
|
|
||||||
def elevated(self, read_deleted=None, overwrite=False):
|
def elevated(self, read_deleted=None, overwrite=False):
|
||||||
"""Return a version of this context with admin flag set."""
|
"""Return a version of this context with admin flag set."""
|
||||||
context = self.deepcopy()
|
context = self.deepcopy()
|
||||||
@ -144,25 +72,3 @@ class RequestContext(context.RequestContext):
|
|||||||
|
|
||||||
def deepcopy(self):
|
def deepcopy(self):
|
||||||
return copy.deepcopy(self)
|
return copy.deepcopy(self)
|
||||||
|
|
||||||
# NOTE(sirp): the openstack/common version of RequestContext uses
|
|
||||||
# tenant/user whereas the Cinder version uses project_id/user_id.
|
|
||||||
# NOTE(adrienverge): The Cinder version of RequestContext now uses
|
|
||||||
# tenant/user internally, so it is compatible with context-aware code from
|
|
||||||
# openstack/common. We still need this shim for the rest of Cinder's
|
|
||||||
# code.
|
|
||||||
@property
|
|
||||||
def project_id(self):
|
|
||||||
return self.tenant
|
|
||||||
|
|
||||||
@project_id.setter
|
|
||||||
def project_id(self, value):
|
|
||||||
self.tenant = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_id(self):
|
|
||||||
return self.user
|
|
||||||
|
|
||||||
@user_id.setter
|
|
||||||
def user_id(self, value):
|
|
||||||
self.user = value
|
|
||||||
|
@ -131,10 +131,6 @@ class GlanceConnectionFailed(JoinException):
|
|||||||
message = "Connection to glance failed: %(reason)s"
|
message = "Connection to glance failed: %(reason)s"
|
||||||
|
|
||||||
|
|
||||||
class ImageLimitExceeded(JoinException):
|
|
||||||
message = "Image quota exceeded"
|
|
||||||
|
|
||||||
|
|
||||||
class ImageNotAuthorized(JoinException):
|
class ImageNotAuthorized(JoinException):
|
||||||
message = "Not authorized for image %(image_id)s."
|
message = "Not authorized for image %(image_id)s."
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ from novajoin.glance import get_default_image_service
|
|||||||
from novajoin.ipa import IPAClient
|
from novajoin.ipa import IPAClient
|
||||||
from novajoin import keystone_client
|
from novajoin import keystone_client
|
||||||
from novajoin.nova import get_instance
|
from novajoin.nova import get_instance
|
||||||
|
from novajoin import policy
|
||||||
from novajoin import util
|
from novajoin import util
|
||||||
|
|
||||||
|
|
||||||
@ -112,6 +113,12 @@ class JoinController(Controller):
|
|||||||
LOG.error('No body in create request')
|
LOG.error('No body in create request')
|
||||||
raise base.Fault(webob.exc.HTTPBadRequest())
|
raise base.Fault(webob.exc.HTTPBadRequest())
|
||||||
|
|
||||||
|
context = req.environ.get('novajoin.context')
|
||||||
|
try:
|
||||||
|
policy.authorize_action(context, 'join:create')
|
||||||
|
except exception.PolicyNotAuthorized:
|
||||||
|
raise base.Fault(webob.exc.HTTPForbidden())
|
||||||
|
|
||||||
instance_id = body.get('instance-id')
|
instance_id = body.get('instance-id')
|
||||||
image_id = body.get('image-id')
|
image_id = body.get('image-id')
|
||||||
project_id = body.get('project-id')
|
project_id = body.get('project-id')
|
||||||
@ -139,7 +146,6 @@ class JoinController(Controller):
|
|||||||
if enroll.lower() != 'true':
|
if enroll.lower() != 'true':
|
||||||
LOG.debug('IPA enrollment not requested in instance creation')
|
LOG.debug('IPA enrollment not requested in instance creation')
|
||||||
|
|
||||||
context = req.environ.get('novajoin.context')
|
|
||||||
image_service = get_default_image_service()
|
image_service = get_default_image_service()
|
||||||
image_metadata = {}
|
image_metadata = {}
|
||||||
try:
|
try:
|
||||||
|
@ -19,8 +19,6 @@ Simplified Common Auth Middleware from cinder.
|
|||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_middleware import request_id
|
|
||||||
from oslo_serialization import jsonutils
|
|
||||||
import webob.dec
|
import webob.dec
|
||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
@ -53,48 +51,10 @@ class JoinKeystoneContext(novajoin.base.Middleware):
|
|||||||
|
|
||||||
@webob.dec.wsgify(RequestClass=novajoin.base.Request)
|
@webob.dec.wsgify(RequestClass=novajoin.base.Request)
|
||||||
def __call__(self, req):
|
def __call__(self, req):
|
||||||
user_id = req.headers.get('X_USER')
|
try:
|
||||||
user_id = req.headers.get('X_USER_ID', user_id)
|
ctx = context.RequestContext.from_environ(req.environ)
|
||||||
if user_id is None:
|
except KeyError:
|
||||||
LOG.debug("Neither X_USER_ID nor X_USER found in request")
|
LOG.debug("Keystone middleware headers not found in request!")
|
||||||
return webob.exc.HTTPUnauthorized()
|
return webob.exc.HTTPUnauthorized()
|
||||||
# get the roles
|
|
||||||
roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')]
|
|
||||||
if 'X_TENANT_ID' in req.headers:
|
|
||||||
# This is the new header since Keystone went to ID/Name
|
|
||||||
project_id = req.headers['X_TENANT_ID']
|
|
||||||
else:
|
|
||||||
# This is for legacy compatibility
|
|
||||||
project_id = req.headers['X_TENANT']
|
|
||||||
|
|
||||||
project_name = req.headers.get('X_TENANT_NAME')
|
|
||||||
|
|
||||||
req_id = req.environ.get(request_id.ENV_REQUEST_ID)
|
|
||||||
|
|
||||||
# Get the auth token
|
|
||||||
auth_token = req.headers.get('X_AUTH_TOKEN',
|
|
||||||
req.headers.get('X_STORAGE_TOKEN'))
|
|
||||||
|
|
||||||
# Build a context, including the auth_token...
|
|
||||||
remote_address = req.remote_addr
|
|
||||||
|
|
||||||
service_catalog = None
|
|
||||||
if req.headers.get('X_SERVICE_CATALOG') is not None:
|
|
||||||
try:
|
|
||||||
catalog_header = req.headers.get('X_SERVICE_CATALOG')
|
|
||||||
service_catalog = jsonutils.loads(catalog_header)
|
|
||||||
except ValueError:
|
|
||||||
raise webob.exc.HTTPInternalServerError(
|
|
||||||
explanation='Invalid service catalog json.')
|
|
||||||
|
|
||||||
ctx = context.RequestContext(user_id,
|
|
||||||
project_id,
|
|
||||||
project_name=project_name,
|
|
||||||
roles=roles,
|
|
||||||
auth_token=auth_token,
|
|
||||||
remote_address=remote_address,
|
|
||||||
service_catalog=service_catalog,
|
|
||||||
request_id=req_id)
|
|
||||||
|
|
||||||
req.environ['novajoin.context'] = ctx
|
req.environ['novajoin.context'] = ctx
|
||||||
return self.application
|
return self.application
|
||||||
|
@ -16,58 +16,81 @@
|
|||||||
"""Policy Engine"""
|
"""Policy Engine"""
|
||||||
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_policy import opts as policy_opts
|
from oslo_policy import opts as policy_opts
|
||||||
from oslo_policy import policy
|
from oslo_policy import policy
|
||||||
|
|
||||||
|
from novajoin import config
|
||||||
from novajoin import exception
|
from novajoin import exception
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = config.CONF
|
||||||
policy_opts.set_defaults(cfg.CONF, 'policy.json')
|
policy_opts.set_defaults(CONF, 'policy.json')
|
||||||
|
|
||||||
_ENFORCER = None
|
_ENFORCER = None
|
||||||
|
|
||||||
|
# We have only one endpoint, so there is not a lot of default rules
|
||||||
|
_RULES = [
|
||||||
|
policy.RuleDefault(
|
||||||
|
'context_is_admin', 'role:admin',
|
||||||
|
"Decides what is required for the 'is_admin:True' check to succeed."),
|
||||||
|
policy.RuleDefault(
|
||||||
|
'service_role', 'role:service',
|
||||||
|
"service role"),
|
||||||
|
policy.RuleDefault(
|
||||||
|
'compute_service_user', 'user_name:nova and rule:service_role',
|
||||||
|
"This is usualy the nova service user, which calls the novajoin API, "
|
||||||
|
"configured in [vendordata_dynamic_auth] in nova.conf."),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
'join:create', 'rule:compute_service_user',
|
||||||
|
'Generate the OTP, register it with IPA',
|
||||||
|
[{'path': '/', 'method': 'POST'}]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def init():
|
|
||||||
|
def list_rules():
|
||||||
|
return _RULES
|
||||||
|
|
||||||
|
|
||||||
|
def get_enforcer():
|
||||||
global _ENFORCER # pylint: disable=global-statement
|
global _ENFORCER # pylint: disable=global-statement
|
||||||
if not _ENFORCER:
|
if not _ENFORCER:
|
||||||
_ENFORCER = policy.Enforcer(CONF)
|
_ENFORCER = policy.Enforcer(CONF)
|
||||||
|
_ENFORCER.register_defaults(list_rules())
|
||||||
|
return _ENFORCER
|
||||||
|
|
||||||
|
|
||||||
def enforce_action(context, action):
|
def authorize_action(context, action):
|
||||||
"""Checks that the action can be done by the given context.
|
"""Checks that the action can be done by the given context.
|
||||||
|
|
||||||
Applies a check to ensure the context's project_id and user_id can be
|
Applies a check to ensure the context's project_id, user_id and others
|
||||||
applied to the given action using the policy enforcement api.
|
can be applied to the given action using the policy enforcement api.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return enforce(context, action, {'project_id': context.project_id,
|
return authorize(context, action, context.to_dict())
|
||||||
'user_id': context.user_id})
|
|
||||||
|
|
||||||
|
|
||||||
def enforce(context, action, target):
|
def authorize(context, action, target):
|
||||||
"""Verifies that the action is valid on the target in this context.
|
"""Verifies that the action is valid on the target in this context.
|
||||||
|
|
||||||
:param context: cinder context
|
:param context: novajoin context
|
||||||
:param action: string representing the action to be checked
|
:param action: string representing the action to be checked
|
||||||
this should be colon separated for clarity.
|
this should be colon separated for clarity.
|
||||||
i.e. ``compute:create_instance``,
|
i.e. ``compute:create_instance``,
|
||||||
``compute:attach_volume``,
|
``compute:attach_volume``,
|
||||||
``volume:attach_volume``
|
``volume:attach_volume``
|
||||||
|
|
||||||
:param object: dictionary representing the object of the action
|
:param target: dictionary representing the object of the action
|
||||||
for object creation this should be a dictionary representing the
|
for object creation this should be a dictionary representing the
|
||||||
location of the object e.g. ``{'project_id': context.project_id}``
|
location of the object e.g. ``{'project_id': context.project_id}``
|
||||||
|
|
||||||
:raises PolicyNotAuthorized: if verification fails.
|
:raises PolicyNotAuthorized: if verification fails.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
init()
|
|
||||||
|
|
||||||
return _ENFORCER.enforce(action, target, context.to_dict(),
|
return get_enforcer().authorize(action, target, context.to_dict(),
|
||||||
do_raise=True,
|
do_raise=True,
|
||||||
exc=exception.PolicyNotAuthorized,
|
exc=exception.PolicyNotAuthorized,
|
||||||
action=action)
|
action=action)
|
||||||
|
|
||||||
|
|
||||||
def check_is_admin(roles, context=None):
|
def check_is_admin(roles, context=None):
|
||||||
@ -76,7 +99,6 @@ def check_is_admin(roles, context=None):
|
|||||||
Can use roles or user_id from context to determine if user is admin.
|
Can use roles or user_id from context to determine if user is admin.
|
||||||
In a multi-domain configuration, roles alone may not be sufficient.
|
In a multi-domain configuration, roles alone may not be sufficient.
|
||||||
"""
|
"""
|
||||||
init()
|
|
||||||
|
|
||||||
# include project_id on target to avoid KeyError if context_is_admin
|
# include project_id on target to avoid KeyError if context_is_admin
|
||||||
# policy definition is missing, and default admin_or_owner rule
|
# policy definition is missing, and default admin_or_owner rule
|
||||||
@ -90,4 +112,4 @@ def check_is_admin(roles, context=None):
|
|||||||
'user_id': context.user_id
|
'user_id': context.user_id
|
||||||
}
|
}
|
||||||
|
|
||||||
return _ENFORCER.enforce('context_is_admin', target, credentials)
|
return get_enforcer().authorize('context_is_admin', target, credentials)
|
||||||
|
@ -34,11 +34,22 @@ class HTTPRequest(webob.Request):
|
|||||||
if 'v1' in args[0]:
|
if 'v1' in args[0]:
|
||||||
kwargs['base_url'] = 'http://localhost/v1'
|
kwargs['base_url'] = 'http://localhost/v1'
|
||||||
use_admin_context = kwargs.pop('use_admin_context', False)
|
use_admin_context = kwargs.pop('use_admin_context', False)
|
||||||
|
use_nova_service_context = kwargs.pop('use_nova_context', True)
|
||||||
version = kwargs.pop('version', '1.0')
|
version = kwargs.pop('version', '1.0')
|
||||||
out = base.Request.blank(*args, **kwargs)
|
out = base.Request.blank(*args, **kwargs)
|
||||||
out.environ['cinder.context'] = FakeRequestContext(
|
if use_nova_service_context:
|
||||||
fake.USER_ID,
|
out.environ['novajoin.context'] = FakeRequestContext(
|
||||||
fake.PROJECT_ID,
|
user_id=fake.USER_ID,
|
||||||
is_admin=use_admin_context)
|
user_name='nova',
|
||||||
|
roles=['service'],
|
||||||
|
project_id=fake.PROJECT_ID,
|
||||||
|
is_admin=use_admin_context)
|
||||||
|
else:
|
||||||
|
out.environ['novajoin.context'] = FakeRequestContext(
|
||||||
|
user_id=fake.USER_ID,
|
||||||
|
user_name='not_nova',
|
||||||
|
roles=['not_service'],
|
||||||
|
project_id=fake.PROJECT_ID,
|
||||||
|
is_admin=use_admin_context)
|
||||||
out.api_version_request = Join(version)
|
out.api_version_request = Join(version)
|
||||||
return out
|
return out
|
||||||
|
@ -18,6 +18,7 @@ from oslo_serialization import jsonutils
|
|||||||
from testtools.matchers import MatchesRegex
|
from testtools.matchers import MatchesRegex
|
||||||
|
|
||||||
from novajoin.base import Fault
|
from novajoin.base import Fault
|
||||||
|
from novajoin import config
|
||||||
from novajoin import join
|
from novajoin import join
|
||||||
from novajoin import test
|
from novajoin import test
|
||||||
from novajoin.tests.unit.api import fakes
|
from novajoin.tests.unit.api import fakes
|
||||||
@ -36,6 +37,7 @@ class JoinTest(test.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.join_controller = join.JoinController()
|
self.join_controller = join.JoinController()
|
||||||
|
config.CONF([])
|
||||||
super(JoinTest, self).setUp()
|
super(JoinTest, self).setUp()
|
||||||
|
|
||||||
def test_no_body(self):
|
def test_no_body(self):
|
||||||
@ -53,6 +55,21 @@ class JoinTest(test.TestCase):
|
|||||||
else:
|
else:
|
||||||
assert(False)
|
assert(False)
|
||||||
|
|
||||||
|
def test_unauthorized(self):
|
||||||
|
body = {'test': 'test'}
|
||||||
|
req = fakes.HTTPRequest.blank('/v1/', use_nova_context=False)
|
||||||
|
req.method = 'POST'
|
||||||
|
req.content_type = "application/json"
|
||||||
|
|
||||||
|
# Not using assertRaises because the exception is wrapped as
|
||||||
|
# a Fault
|
||||||
|
try:
|
||||||
|
self.join_controller.create(req, body)
|
||||||
|
except Fault as fault:
|
||||||
|
assert fault.status_int == 403
|
||||||
|
else:
|
||||||
|
assert(False)
|
||||||
|
|
||||||
def test_no_instanceid(self):
|
def test_no_instanceid(self):
|
||||||
body = {"metadata": {"ipa_enroll": "True"},
|
body = {"metadata": {"ipa_enroll": "True"},
|
||||||
"image-id": fake.IMAGE_ID,
|
"image-id": fake.IMAGE_ID,
|
||||||
|
@ -73,3 +73,9 @@ oslo.config.opts.defaults =
|
|||||||
console_scripts =
|
console_scripts =
|
||||||
novajoin-server = novajoin.wsgi:main
|
novajoin-server = novajoin.wsgi:main
|
||||||
novajoin-notify = novajoin.notifications:main
|
novajoin-notify = novajoin.notifications:main
|
||||||
|
|
||||||
|
oslo.policy.policies =
|
||||||
|
novajoin = novajoin.policy:list_rules
|
||||||
|
|
||||||
|
oslo.policy.enforcer =
|
||||||
|
novajoin = novajoin.policy:get_enforcer
|
||||||
|
6
tox.ini
6
tox.ini
@ -65,6 +65,7 @@ commands = sphinx-build -W -b html doc/source doc/build/html
|
|||||||
|
|
||||||
[testenv:genconfig]
|
[testenv:genconfig]
|
||||||
sitepackages = False
|
sitepackages = False
|
||||||
|
basepython = python3
|
||||||
envdir = {toxworkdir}/pep8
|
envdir = {toxworkdir}/pep8
|
||||||
commands = oslo-config-generator --config-file=files/novajoin-config-generator.conf
|
commands = oslo-config-generator --config-file=files/novajoin-config-generator.conf
|
||||||
|
|
||||||
@ -86,3 +87,8 @@ setenv =
|
|||||||
commands =
|
commands =
|
||||||
/usr/bin/find . -type f -name "*.py[c|o]" -delete
|
/usr/bin/find . -type f -name "*.py[c|o]" -delete
|
||||||
stestr run --slowest {posargs}
|
stestr run --slowest {posargs}
|
||||||
|
|
||||||
|
[testenv:genpolicy]
|
||||||
|
basepython = python3
|
||||||
|
envdir = {toxworkdir}/pep8
|
||||||
|
commands = oslopolicy-sample-generator --config-file files/policy-generator.conf
|
||||||
|
Loading…
Reference in New Issue
Block a user