feat(api): policy enforcement and api standard

- enhanced logging
- created base structure
- updated docs
- PasteDeploy auth
- Oslo Policy

Closes #107

Change-Id: I805863c57f17fcfb26dac5d03efb165e4be49a4e
This commit is contained in:
gardlt 2017-09-12 03:11:57 +00:00
parent d5f4378731
commit bb26131ce2
33 changed files with 883 additions and 365 deletions

View File

@ -28,8 +28,7 @@ RUN apt-get update && \
\ \
apt-get purge --auto-remove -y \ apt-get purge --auto-remove -y \
build-essential \ build-essential \
curl \ curl && \
python-all-dev && \
apt-get clean -y && \ apt-get clean -y && \
rm -rf \ rm -rf \
/root/.cache \ /root/.cache \

View File

@ -1,4 +1,4 @@
# Copyright 2017 The Armada Authors. # Copyright 2017 The Armada Authors. All other rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -11,3 +11,139 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json
import uuid
import logging as log
import falcon
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
class BaseResource(object):
def __init__(self):
self.logger = LOG
def on_options(self, req, resp):
self_attrs = dir(self)
methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH']
allowed_methods = []
for m in methods:
if 'on_' + m.lower() in self_attrs:
allowed_methods.append(m)
resp.headers['Allow'] = ','.join(allowed_methods)
resp.status = falcon.HTTP_200
def req_json(self, req):
if req.content_length is None or req.content_length == 0:
return None
if req.content_type is not None and req.content_type.lower(
) == 'application/json':
raw_body = req.stream.read(req.content_length or 0)
if raw_body is None:
return None
try:
# json_body = json.loads(raw_body.decode('utf-8'))
# return json_body
return raw_body
except json.JSONDecodeError as jex:
self.error(
req.context,
"Invalid JSON in request: \n%s" % raw_body.decode('utf-8'))
raise json.JSONDecodeError("%s: Invalid JSON in body: %s" %
(req.path, jex))
else:
raise json.JSONDecodeError("Requires application/json payload")
def return_error(self, resp, status_code, message="", retry=False):
resp.body = json.dumps({
'type': 'error',
'message': message,
'retry': retry
})
resp.status = status_code
def log_error(self, ctx, level, msg):
extra = {'user': 'N/A', 'req_id': 'N/A', 'external_ctx': 'N/A'}
if ctx is not None:
extra = {
'user': ctx.user,
'req_id': ctx.request_id,
'external_ctx': ctx.external_marker,
}
self.logger.log(level, msg, extra=extra)
def debug(self, ctx, msg):
self.log_error(ctx, log.DEBUG, msg)
def info(self, ctx, msg):
self.log_error(ctx, log.INFO, msg)
def warn(self, ctx, msg):
self.log_error(ctx, log.WARN, msg)
def error(self, ctx, msg):
self.log_error(ctx, log.ERROR, msg)
class ArmadaRequestContext(object):
def __init__(self):
self.log_level = 'ERROR'
self.user = None # Username
self.user_id = None # User ID (UUID)
self.user_domain_id = None # Domain owning user
self.roles = ['anyone']
self.project_id = None
self.project_domain_id = None # Domain owning project
self.is_admin_project = False
self.authenticated = False
self.request_id = str(uuid.uuid4())
self.external_marker = ''
def set_log_level(self, level):
if level in ['error', 'info', 'debug']:
self.log_level = level
def set_user(self, user):
self.user = user
def set_project(self, project):
self.project = project
def add_role(self, role):
self.roles.append(role)
def add_roles(self, roles):
self.roles.extend(roles)
def remove_role(self, role):
self.roles = [x for x in self.roles if x != role]
def set_external_marker(self, marker):
self.external_marker = marker
def to_policy_view(self):
policy_dict = {}
policy_dict['user_id'] = self.user_id
policy_dict['user_domain_id'] = self.user_domain_id
policy_dict['project_id'] = self.project_id
policy_dict['project_domain_id'] = self.project_domain_id
policy_dict['roles'] = self.roles
policy_dict['is_admin_project'] = self.is_admin_project
return policy_dict
class ArmadaRequest(falcon.request.Request):
context_type = ArmadaRequestContext

View File

@ -13,41 +13,47 @@
# limitations under the License. # limitations under the License.
import json import json
from falcon import HTTP_200
from oslo_config import cfg import falcon
from oslo_log import log as logging from oslo_log import log as logging
from armada.handlers.armada import Armada as Handler from armada import api
from armada.handlers.armada import Armada
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class Apply(api.BaseResource):
class Apply(object):
''' '''
apply armada endpoint service apply armada endpoint service
''' '''
def on_post(self, req, resp): def on_post(self, req, resp):
try:
# Load data from request and get options # Load data from request and get options
data = json.load(req.stream) data = self.req_json(req)
opts = data['options'] opts = {}
# opts = data['options']
# Encode filename # Encode filename
data['file'] = data['file'].encode('utf-8') # data['file'] = data['file'].encode('utf-8')
armada = Armada(
data,
disable_update_pre=opts.get('disable_update_pre', False),
disable_update_post=opts.get('disable_update_post', False),
enable_chart_cleanup=opts.get('enable_chart_cleanup', False),
dry_run=opts.get('dry_run', False),
wait=opts.get('wait', False),
timeout=opts.get('timeout', False))
armada = Handler(open('../../' + data['file']), msg = armada.sync()
disable_update_pre=opts['disable_update_pre'],
disable_update_post=opts['disable_update_post'],
enable_chart_cleanup=opts['enable_chart_cleanup'],
dry_run=opts['dry_run'],
wait=opts['wait'],
timeout=opts['timeout'])
armada.sync() resp.data = json.dumps({'message': msg})
resp.data = json.dumps({'message': 'Success'}) resp.content_type = 'application/json'
resp.content_type = 'application/json' resp.status = falcon.HTTP_200
resp.status = HTTP_200 except Exception as e:
self.error(req.context, "Failed to apply manifest")
self.return_error(
resp, falcon.HTTP_500,
message="Failed to install manifest: {} {}".format(e, data))

View File

@ -12,10 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import falcon from uuid import UUID
from keystoneauth1 import session
from keystoneauth1.identity import v3
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
@ -25,75 +23,80 @@ CONF = cfg.CONF
class AuthMiddleware(object): class AuthMiddleware(object):
# Authentication
def process_request(self, req, resp): def process_request(self, req, resp):
ctx = req.context
# Validate token and get user session for k, v in req.headers.items():
token = req.get_header('X-Auth-Token') LOG.debug("Request with header %s: %s" % (k, v))
req.context['session'] = self._get_user_session(token)
# Add token roles to request context auth_status = req.get_header('X-SERVICE-IDENTITY-STATUS')
req.context['roles'] = self._get_roles(req.context['session']) service = True
def _get_roles(self, session): if auth_status is None:
auth_status = req.get_header('X-IDENTITY-STATUS')
service = False
# Get roles IDs associated with user if auth_status == 'Confirmed':
request_url = CONF.auth_url + '/role_assignments' # Process account and roles
resp = self._session_request(session=session, request_url=request_url) ctx.authenticated = True
ctx.user = req.get_header(
'X-SERVICE-USER-NAME') if service else req.get_header(
'X-USER-NAME')
ctx.user_id = req.get_header(
'X-SERVICE-USER-ID') if service else req.get_header(
'X-USER-ID')
ctx.user_domain_id = req.get_header(
'X-SERVICE-USER-DOMAIN-ID') if service else req.get_header(
'X-USER-DOMAIN-ID')
ctx.project_id = req.get_header(
'X-SERVICE-PROJECT-ID') if service else req.get_header(
'X-PROJECT-ID')
ctx.project_domain_id = req.get_header(
'X-SERVICE-PROJECT-DOMAIN-ID') if service else req.get_header(
'X-PROJECT-DOMAIN-NAME')
if service:
ctx.add_roles(req.get_header('X-SERVICE-ROLES').split(','))
else:
ctx.add_roles(req.get_header('X-ROLES').split(','))
json_resp = resp.json()['role_assignments'] if req.get_header('X-IS-ADMIN-PROJECT') == 'True':
role_ids = [r['role']['id'].encode('utf-8') for r in json_resp] ctx.is_admin_project = True
else:
ctx.is_admin_project = False
# Get role names associated with role IDs LOG.debug('Request from authenticated user %s with roles %s' %
roles = [] (ctx.user, ','.join(ctx.roles)))
for role_id in role_ids: else:
request_url = CONF.auth_url + '/roles/' + role_id ctx.authenticated = False
resp = self._session_request(session=session,
request_url=request_url)
role = resp.json()['role']['name'].encode('utf-8')
roles.append(role)
return roles class ContextMiddleware(object):
def _get_user_session(self, token): def process_request(self, req, resp):
ctx = req.context
# Get user session from token ext_marker = req.get_header('X-Context-Marker')
auth = v3.Token(auth_url=CONF.auth_url,
project_name=CONF.project_name,
project_domain_name=CONF.project_domain_name,
token=token)
return session.Session(auth=auth) if ext_marker is not None and self.is_valid_uuid(ext_marker):
ctx.set_external_marker(ext_marker)
def _session_request(self, session, request_url): def is_valid_uuid(self, id, version=4):
try: try:
return session.get(request_url) uuid_obj = UUID(id, version=version)
except: except:
raise falcon.HTTPUnauthorized('Authentication required', return False
('Authentication token is invalid.'))
class RoleMiddleware(object): return str(uuid_obj) == id
def process_request(self, req, resp):
endpoint = req.path
roles = req.context['roles']
# Verify roles have sufficient permissions for request endpoint class LoggingMiddleware(object):
if not (self._verify_roles(endpoint, roles)): def process_response(self, req, resp, resource, req_succeeded):
raise falcon.HTTPUnauthorized('Insufficient permissions', ctx = req.context
('Token role insufficient.')) extra = {
'user': ctx.user,
def _verify_roles(self, endpoint, roles): 'req_id': ctx.request_id,
'external_ctx': ctx.external_marker,
# Compare the verified roles listed in the config with the user's }
# associated roles resp.append_header('X-Armada-Req', ctx.request_id)
if endpoint == '/armada/apply': LOG.info("%s - %s" % (req.uri, resp.status), extra=extra)
approved_roles = CONF.armada_apply_roles
elif endpoint == '/tiller/releases':
approved_roles = CONF.tiller_release_roles
elif endpoint == '/tiller/status':
approved_roles = CONF.tiller_status_roles
verified_roles = set(roles).intersection(approved_roles)
return bool(verified_roles)

View File

@ -13,42 +13,63 @@
# limitations under the License. # limitations under the License.
import falcon import falcon
import os
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
import armada.conf as configs from armada.common import policy
from armada import conf
from armada.api import ArmadaRequest
from armada_controller import Apply from armada_controller import Apply
from middleware import AuthMiddleware from middleware import AuthMiddleware
from middleware import RoleMiddleware from middleware import ContextMiddleware
from middleware import LoggingMiddleware
from tiller_controller import Release from tiller_controller import Release
from tiller_controller import Status from tiller_controller import Status
from validation_controller import Validate
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
configs.set_app_default_configs() conf.set_app_default_configs()
CONF = cfg.CONF CONF = cfg.CONF
# Build API # Build API
def create(middleware=CONF.middleware): def create(middleware=CONF.middleware):
logging.register_options(CONF) if not (os.path.exists('etc/armada/armada.conf')):
logging.set_defaults(default_log_levels=CONF.default_log_levels) logging.register_options(CONF)
logging.setup(CONF, 'armada') logging.set_defaults(default_log_levels=CONF.default_log_levels)
logging.setup(CONF, 'armada')
policy.setup_policy()
if middleware: if middleware:
api = falcon.API(middleware=[AuthMiddleware(), RoleMiddleware()]) api = falcon.API(
request_type=ArmadaRequest,
middleware=[
AuthMiddleware(),
LoggingMiddleware(),
ContextMiddleware()
])
else: else:
api = falcon.API() api = falcon.API(request_type=ArmadaRequest)
# Configure API routing # Configure API routing
url_routes = ( url_routes_v1 = (('apply', Apply()),
('/tiller/status', Status()), ('releases', Release()),
('/tiller/releases', Release()), ('status', Status()),
('/armada/apply/', Apply()) ('validate', Validate()))
)
for route, service in url_routes: for route, service in url_routes_v1:
api.add_route(route, service) api.add_route("/v1.0/{}".format(route), service)
return api
def paste_start_armada(global_conf, **kwargs):
# At this time just ignore everything in the paste configuration
# and rely on olso_config
return api return api

View File

@ -13,45 +13,66 @@
# limitations under the License. # limitations under the License.
import json import json
from falcon import HTTP_200
import falcon
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 armada.handlers.tiller import Tiller as tillerHandler from armada import api
from armada.common import policy
from armada.handlers.tiller import Tiller
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
class Status(object): class Status(api.BaseResource):
@policy.enforce('tiller:get_status')
def on_get(self, req, resp): def on_get(self, req, resp):
''' '''
get tiller status get tiller status
''' '''
message = "Tiller Server is {}" try:
if tillerHandler().tiller_status(): message = {'tiller': Tiller().tiller_status()}
resp.data = json.dumps({'message': message.format('Active')})
LOG.info('Tiller Server is Active.')
else:
resp.data = json.dumps({'message': message.format('Not Present')})
LOG.info('Tiller Server is Not Present.')
resp.content_type = 'application/json' if message.get('tiller', False):
resp.status = HTTP_200 resp.status = falcon.HTTP_200
else:
resp.status = falcon.HTTP_503
class Release(object): resp.data = json.dumps(message)
resp.content_type = 'application/json'
except Exception as e:
self.error(req.context, "Unable to find resources")
self.return_error(
resp, falcon.HTTP_500,
message="Unable to get status: {}".format(e))
class Release(api.BaseResource):
@policy.enforce('tiller:get_release')
def on_get(self, req, resp): def on_get(self, req, resp):
''' '''
get tiller releases get tiller releases
''' '''
# Get tiller releases try:
handler = tillerHandler() # Get tiller releases
handler = Tiller()
releases = {} releases = {}
for release in handler.list_releases(): for release in handler.list_releases():
releases[release.name] = release.namespace if not releases.get(release.namespace, None):
releases[release.namespace] = []
resp.data = json.dumps({'releases': releases}) releases[release.namespace].append(release.name)
resp.content_type = 'application/json'
resp.status = HTTP_200 resp.data = json.dumps({'releases': releases})
resp.content_type = 'application/json'
resp.status = falcon.HTTP_200
except Exception as e:
self.error(req.context, "Unable to find resources")
self.return_error(
resp, falcon.HTTP_500,
message="Unable to find Releases: {}".format(e))

View File

@ -0,0 +1,56 @@
# Copyright 2017 The Armada Authors.
#
# 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 json
import yaml
import falcon
from oslo_log import log as logging
from armada import api
from armada.common import policy
from armada.utils.lint import validate_armada_documents
LOG = logging.getLogger(__name__)
class Validate(api.BaseResource):
'''
apply armada endpoint service
'''
@policy.enforce('armada:validate_manifest')
def on_post(self, req, resp):
try:
message = {
'valid':
validate_armada_documents(
list(yaml.safe_load_all(self.req_json(req))))
}
if message.get('valid', False):
resp.status = falcon.HTTP_200
else:
resp.status = falcon.HTTP_400
resp.data = json.dumps(message)
resp.content_type = 'application/json'
except Exception:
self.error(req.context, "Failed: Invalid Armada Manifest")
self.return_error(
resp,
falcon.HTTP_400,
message="Failed: Invalid Armada Manifest")

View File

19
armada/common/i18n.py Normal file
View File

@ -0,0 +1,19 @@
# 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 oslo_i18n
_translators = oslo_i18n.TranslatorFactory(domain='armada')
# The primary translation function using the well-known name "_"
_ = _translators.primary

View File

@ -0,0 +1,25 @@
# 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 armada.common.policies import base
from armada.common.policies import service
from armada.common.policies import tiller
def list_rules():
return itertools.chain(
base.list_rules(),
service.list_rules(),
tiller.list_rules()
)

View File

@ -0,0 +1,34 @@
# 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
ARMADA = 'armada:%s'
TILLER = 'tiller:%s'
RULE_ADMIN_REQUIRED = 'rule:admin_required'
RULE_ADMIN_OR_TARGET_PROJECT = (
'rule:admin_required or project_id:%(target.project.id)s')
RULE_SERVICE_OR_ADMIN = 'rule:service_or_admin'
rules = [
policy.RuleDefault(name='admin_required',
check_str='role:admin'),
policy.RuleDefault(name='service_or_admin',
check_str='rule:admin_required or rule:service_role'),
policy.RuleDefault(name='service_role',
check_str='role:service'),
]
def list_rules():
return rules

View File

@ -0,0 +1,33 @@
# 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 armada.common.policies import base
armada_policies = [
policy.DocumentedRuleDefault(
name=base.ARMADA % 'create_endpoints',
check_str=base.RULE_ADMIN_REQUIRED,
description='install manifest charts',
operations=[{'path': '/v1.0/apply/', 'method': 'POST'}]),
policy.DocumentedRuleDefault(
name=base.ARMADA % 'validate_manifest',
check_str=base.RULE_ADMIN_REQUIRED,
description='validate install manifest',
operations=[{'path': '/v1.0/validate/', 'method': 'POST'}]),
]
def list_rules():
return armada_policies

View File

@ -0,0 +1,36 @@
# 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 armada.common.policies import base
tiller_policies = [
policy.DocumentedRuleDefault(
name=base.TILLER % 'get_status',
check_str=base.RULE_ADMIN_REQUIRED,
description='Get tiller status',
operations=[{'path': '/v1.0/status/',
'method': 'GET'}]),
policy.DocumentedRuleDefault(
name=base.TILLER % 'get_release',
check_str=base.RULE_ADMIN_REQUIRED,
description='Get tiller release',
operations=[{'path': '/v1.0/releases/',
'method': 'GET'}]),
]
def list_rules():
return tiller_policies

57
armada/common/policy.py Normal file
View File

@ -0,0 +1,57 @@
# 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 functools
from oslo_config import cfg
from oslo_policy import policy
from armada.common import policies
from armada.exceptions import base_exception as exc
CONF = cfg.CONF
_ENFORCER = None
def setup_policy():
global _ENFORCER
if not _ENFORCER:
_ENFORCER = policy.Enforcer(CONF)
register_rules(_ENFORCER)
def enforce_policy(action, target, credentials, do_raise=True):
extras = {}
if do_raise:
extras.update(exc=exc.ActionForbidden, do_raise=do_raise)
_ENFORCER.enforce(action, target, credentials.to_policy_view(), **extras)
def enforce(rule):
setup_policy()
def decorator(func):
@functools.wraps(func)
def handler(*args, **kwargs):
context = args[1].context
enforce_policy(rule, {}, context, do_raise=True)
return func(*args, **kwargs)
return handler
return decorator
def register_rules(enforcer):
enforcer.register_defaults(policies.list_rules())

View File

@ -0,0 +1,32 @@
# Copyright 2017 The Armada Authors.
#
# 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 base_exception as base
class ApiException(base.ArmadaBaseException):
'''Base class for API exceptions and error handling.'''
message = 'An unknown API error occur.'
class ApiBaseException(ApiException):
'''Exception that occurs during chart cleanup.'''
message = 'There was an error listing the helm chart releases.'
class ApiJsonException(ApiException):
'''Exception that occurs during chart cleanup.'''
message = 'There was an error listing the helm chart releases.'

View File

@ -12,9 +12,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import falcon
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 armada.common.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 3600 DEFAULT_TIMEOUT = 3600
@ -27,3 +30,21 @@ class ArmadaBaseException(Exception):
def __init__(self, message=None): def __init__(self, message=None):
self.message = message or self.message self.message = message or self.message
super(ArmadaBaseException, self).__init__(self.message) super(ArmadaBaseException, self).__init__(self.message)
class ArmadaAPIException(falcon.HTTPError):
status = falcon.HTTP_500
message = "unknown error"
title = "Internal Server Error"
def __init__(self, message=None, **kwargs):
self.message = message or self.message
super(ArmadaAPIException, self).__init__(
self.status, self.title, self.message, **kwargs
)
class ActionForbidden(ArmadaAPIException):
status = falcon.HTTP_403
message = _("Insufficient privilege to perform action.")
title = _("Action Forbidden")

View File

@ -1,13 +0,0 @@
# Copyright 2017 The Armada Authors.
#
# 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.

View File

@ -15,7 +15,6 @@
import difflib import difflib
import yaml import yaml
from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from supermutes.dot import dotify from supermutes.dot import dotify
@ -37,7 +36,6 @@ from ..const import KEYWORD_ARMADA, KEYWORD_GROUPS, KEYWORD_CHARTS,\
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 3600 DEFAULT_TIMEOUT = 3600
CONF = cfg.CONF
class Armada(object): class Armada(object):
@ -183,6 +181,12 @@ class Armada(object):
Syncronize Helm with the Armada Config(s) Syncronize Helm with the Armada Config(s)
''' '''
msg = {
'installed': [],
'upgraded': [],
'diff': []
}
# TODO: (gardlt) we need to break up this func into # TODO: (gardlt) we need to break up this func into
# a more cleaner format # a more cleaner format
LOG.info("Performing Pre-Flight Operations") LOG.info("Performing Pre-Flight Operations")
@ -268,9 +272,9 @@ class Armada(object):
# TODO(alanmeadows) account for .files differences # TODO(alanmeadows) account for .files differences
# once we support those # once we support those
upgrade_diff = self.show_diff(chart, apply_chart, upgrade_diff = self.show_diff(
apply_values, chart, apply_chart, apply_values, chartbuilder.dump(),
chartbuilder.dump(), values) values, msg)
if not upgrade_diff: if not upgrade_diff:
LOG.info("There are no updates found in this chart") LOG.info("There are no updates found in this chart")
@ -290,6 +294,8 @@ class Armada(object):
wait=chart_wait, wait=chart_wait,
timeout=chart_timeout) timeout=chart_timeout)
msg['upgraded'].append(prefix_chart)
# process install # process install
else: else:
LOG.info("Installing release %s", chart.release) LOG.info("Installing release %s", chart.release)
@ -301,6 +307,8 @@ class Armada(object):
wait=chart_wait, wait=chart_wait,
timeout=chart_timeout) timeout=chart_timeout)
msg['installed'].append(prefix_chart)
LOG.debug("Cleaning up chart source in %s", LOG.debug("Cleaning up chart source in %s",
chartbuilder.source_directory) chartbuilder.source_directory)
@ -322,6 +330,8 @@ class Armada(object):
self.tiller.chart_cleanup( self.tiller.chart_cleanup(
prefix, self.config[KEYWORD_ARMADA][KEYWORD_GROUPS]) prefix, self.config[KEYWORD_ARMADA][KEYWORD_GROUPS])
return msg
def post_flight_ops(self): def post_flight_ops(self):
''' '''
Operations to run after deployment process has terminated Operations to run after deployment process has terminated
@ -333,7 +343,7 @@ class Armada(object):
source.source_cleanup(ch.get('chart').get('source_dir')[0]) source.source_cleanup(ch.get('chart').get('source_dir')[0])
def show_diff(self, chart, installed_chart, installed_values, target_chart, def show_diff(self, chart, installed_chart, installed_values, target_chart,
target_values): target_values, msg):
''' '''
Produce a unified diff of the installed chart vs our intention Produce a unified diff of the installed chart vs our intention
@ -342,19 +352,32 @@ class Armada(object):
''' '''
chart_diff = list( chart_diff = list(
difflib.unified_diff(installed_chart.SerializeToString() difflib.unified_diff(
.split('\n'), target_chart.split('\n'))) installed_chart.SerializeToString().split('\n'),
target_chart.split('\n')))
if len(chart_diff) > 0: if len(chart_diff) > 0:
LOG.info("Chart Unified Diff (%s)", chart.release) LOG.info("Chart Unified Diff (%s)", chart.release)
diff_msg = []
for line in chart_diff: for line in chart_diff:
diff_msg.append(line)
LOG.debug(line) LOG.debug(line)
msg['diff'].append({'chart': diff_msg})
values_diff = list( values_diff = list(
difflib.unified_diff( difflib.unified_diff(
installed_values.split('\n'), installed_values.split('\n'),
yaml.safe_dump(target_values).split('\n'))) yaml.safe_dump(target_values).split('\n')))
if len(values_diff) > 0: if len(values_diff) > 0:
LOG.info("Values Unified Diff (%s)", chart.release) LOG.info("Values Unified Diff (%s)", chart.release)
diff_msg = []
for line in values_diff: for line in values_diff:
diff_msg.append(line)
LOG.debug(line) LOG.debug(line)
msg['diff'].append({'values': diff_msg})
return (len(chart_diff) > 0) or (len(values_diff) > 0) result = (len(chart_diff) > 0) or (len(values_diff) > 0)
return result

View File

@ -16,16 +16,21 @@ import json
import mock import mock
import unittest import unittest
import falcon
from falcon import testing from falcon import testing
from armada import conf as cfg
from armada.api import server from armada.api import server
CONF = cfg.CONF
class APITestCase(testing.TestCase): class APITestCase(testing.TestCase):
def setUp(self): def setUp(self):
super(APITestCase, self).setUp() super(APITestCase, self).setUp()
self.app = server.create(middleware=False) self.app = server.create(middleware=False)
class TestAPI(APITestCase): class TestAPI(APITestCase):
@unittest.skip('this is incorrectly tested') @unittest.skip('this is incorrectly tested')
@mock.patch('armada.api.armada_controller.Handler') @mock.patch('armada.api.armada_controller.Handler')
@ -35,7 +40,7 @@ class TestAPI(APITestCase):
''' '''
mock_armada.sync.return_value = None mock_armada.sync.return_value = None
body = json.dumps({'file': '../examples/openstack-helm.yaml', body = json.dumps({'file': '',
'options': {'debug': 'true', 'options': {'debug': 'true',
'disable_update_pre': 'false', 'disable_update_pre': 'false',
'disable_update_post': 'false', 'disable_update_post': 'false',
@ -50,10 +55,10 @@ class TestAPI(APITestCase):
result = self.simulate_post(path='/armada/apply', body=body) result = self.simulate_post(path='/armada/apply', body=body)
self.assertEqual(result.json, doc) self.assertEqual(result.json, doc)
@mock.patch('armada.api.tiller_controller.tillerHandler') @mock.patch('armada.api.tiller_controller.Tiller')
def test_tiller_status(self, mock_tiller): def test_tiller_status(self, mock_tiller):
''' '''
Test /tiller/status endpoint Test /status endpoint
''' '''
# Mock tiller status value # Mock tiller status value
@ -61,10 +66,17 @@ class TestAPI(APITestCase):
doc = {u'message': u'Tiller Server is Active'} doc = {u'message': u'Tiller Server is Active'}
result = self.simulate_get('/tiller/status') result = self.simulate_get('/v1.0/status')
self.assertEqual(result.json, doc)
@mock.patch('armada.api.tiller_controller.tillerHandler') # TODO(lamt) This should be HTTP_401 if no auth is happening, but auth
# is not implemented currently, so it falls back to a policy check
# failure, thus a 403. Change this once it is completed
self.assertEqual(falcon.HTTP_403, result.status)
# FIXME(lamt) Need authentication - mock, fixture
# self.assertEqual(result.json, doc)
@mock.patch('armada.api.tiller_controller.Tiller')
def test_tiller_releases(self, mock_tiller): def test_tiller_releases(self, mock_tiller):
''' '''
Test /tiller/releases endpoint Test /tiller/releases endpoint
@ -75,5 +87,12 @@ class TestAPI(APITestCase):
doc = {u'releases': {}} doc = {u'releases': {}}
result = self.simulate_get('/tiller/releases') result = self.simulate_get('/v1.0/releases')
self.assertEqual(result.json, doc)
# TODO(lamt) This should be HTTP_401 if no auth is happening, but auth
# is not implemented currently, so it falls back to a policy check
# failure, thus a 403. Change this once it is completed
self.assertEqual(falcon.HTTP_403, result.status)
# FIXME(lamt) Need authentication - mock, fixture
# self.assertEqual(result.json, doc)

View File

@ -0,0 +1,65 @@
# 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 testtools
from oslo_policy import policy as common_policy
from armada.common import policy
from armada import conf as cfg
from armada.exceptions import base_exception as exc
import mock
CONF = cfg.CONF
class PolicyTestCase(testtools.TestCase):
def setUp(self):
super(PolicyTestCase, self).setUp()
self.rules = {
"true": [],
"example:allowed": [],
"example:disallowed": [["false:false"]]
}
self._set_rules()
self.credentials = {}
self.target = {}
def _set_rules(self):
curr_rules = common_policy.Rules.from_dict(self.rules)
policy._ENFORCER.set_rules(curr_rules)
@mock.patch('armada.api.ArmadaRequestContext')
def test_enforce_nonexistent_action(self, mock_ctx):
action = "example:nope"
mock_ctx.to_policy_view.return_value = self.credentials
self.assertRaises(
exc.ActionForbidden, policy.enforce_policy, action,
self.target, mock_ctx)
@mock.patch('armada.api.ArmadaRequestContext')
def test_enforce_good_action(self, mock_ctx):
action = "example:allowed"
mock_ctx.to_policy_view.return_value = self.credentials
policy.enforce_policy(action, self.target, mock_ctx)
@mock.patch('armada.api.ArmadaRequestContext')
def test_enforce_bad_action(self, mock_ctx):
action = "example:disallowed"
mock_ctx.to_policy_view.return_value = self.credentials
self.assertRaises(exc.ActionForbidden, policy.enforce_policy,
action, self.target, mock_ctx)

View File

@ -13,9 +13,17 @@ To use the docker containter to develop:
.. code-block:: bash .. code-block:: bash
git clone http://github.com/att-comdev/armada.git
cd armada
pip install tox
tox -e genconfig
tox -e genpolicy
docker build . -t armada/latest docker build . -t armada/latest
docker run -d --name armada -v ~/.kube/config:/armada/.kube/config -v $(pwd)/examples/:/examples armada/latest docker run -d --name armada -v ~/.kube/config:/armada/.kube/config -v $(pwd)/etc:/armada/etc armada:local
.. note:: .. note::
@ -25,17 +33,21 @@ To use the docker containter to develop:
Virtualenv Virtualenv
########## ##########
To use VirtualEnv: How to set up armada in your local using virtualenv:
1. virtualenv venv .. note::
2. source ./venv/bin/activate
Suggest that you use a Ubuntu 16.04 VM
From the directory of the forked repository: From the directory of the forked repository:
.. code-block:: bash .. code-block:: bash
pip install -r requirements.txt
pip install -r test-requirements.txt git clone http://github.com/att-comdev/armada.git && cd armada
virtualenv venv
pip install -r requirements.txt -r test-requirements.txt
pip install . pip install .
@ -48,6 +60,12 @@ From the directory of the forked repository:
tox -e bandit tox -e bandit
tox -e cover tox -e cover
# policy and config are used in order to use and configure Armada API
tox -e genconfig
tox -e genpolicy
.. note:: .. note::
If building from source, Armada requires that git be installed on If building from source, Armada requires that git be installed on

View File

@ -0,0 +1,52 @@
==================
Configuring Armada
==================
Armada uses an INI-like standard oslo_config file. A sample
file can be generated via tox
.. code-block:: bash
$ tox -e genconfig
Customize your configuration based on the information below
Keystone Integration
====================
Armada requires a service account to use for validating API
tokens
.. note::
If you do not have a keystone already deploy, then armada can deploy a keystone service.
armada apply keystone-manifest.yaml
.. code-block:: bash
$ openstack domain create 'ucp'
$ openstack project create --domain 'ucp' 'service'
$ openstack user create --domain ucp --project service --project-domain 'ucp' --password armada armada
$ openstack role add --project-domain ucp --user-domain ucp --user armada --project service admin
# OR
$ ./tools/keystone-account.sh
The service account must then be included in the armada.conf
.. code-block:: ini
[keystone_authtoken]
auth_uri = https://<keystone-api>:5000/
auth_version = 3
delay_auth_decision = true
auth_type = password
auth_url = https://<keystone-api>:35357/
project_name = service
project_domain_name = ucp
user_name = armada
user_domain_name = ucp
password = armada

View File

@ -10,7 +10,8 @@ Operations Guide
:maxdepth: 2 :maxdepth: 2
:caption: Contents: :caption: Contents:
guide-troubleshooting.rst
guide-build-armada-yaml.rst
guide-use-armada.rst
guide-api.rst guide-api.rst
guide-build-armada-yaml.rst
guide-configure.rst
guide-troubleshooting.rst
guide-use-armada.rst

View File

@ -6,7 +6,7 @@ PORT="8000"
set -e set -e
if [ "$1" = 'server' ]; then if [ "$1" = 'server' ]; then
exec gunicorn server:api -b :$PORT --chdir armada/api exec uwsgi --http 0.0.0.0:${PORT} --paste config:$(pwd)/etc/armada/api-paste.ini --enable-threads -L --pyargv " --config-file $(pwd)/etc/armada/armada.conf"
fi fi
if [ "$1" = 'tiller' ] || [ "$1" = 'apply' ]; then if [ "$1" = 'tiller' ] || [ "$1" = 'apply' ]; then

8
etc/armada/api-paste.ini Normal file
View File

@ -0,0 +1,8 @@
[app:armada-api]
paste.app_factory = armada.api.server:paste_start_armada
[pipeline:main]
pipeline = authtoken armada-api
[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory

View File

@ -1,140 +0,0 @@
[DEFAULT]
#
# From armada.conf
#
# IDs of approved API access roles. (list value)
#armada_apply_roles = admin
# The default Keystone authentication url. (string value)
#auth_url = http://0.0.0.0/v3
# Path to Kubernetes configurations. (string value)
#kubernetes_config_path = /home/user/.kube/
# Enables or disables Keystone authentication middleware. (boolean value)
#middleware = true
# The Keystone project domain name used for authentication. (string value)
#project_domain_name = default
# The Keystone project name used for authentication. (string value)
#project_name = admin
# Path to SSH private key. (string value)
#ssh_key_path = /home/user/.ssh/
# IDs of approved API access roles. (list value)
#tiller_release_roles = admin
# IDs of approved API access roles. (list value)
#tiller_status_roles = admin
#
# From oslo.log
#
# If set to true, the logging level will be set to DEBUG instead of the default
# INFO level. (boolean value)
# Note: This option can be changed without restarting.
#debug = false
# The name of a logging configuration file. This file is appended to any
# existing logging configuration files. For details about logging configuration
# files, see the Python logging module documentation. Note that when logging
# configuration files are used then all logging configuration is set in the
# configuration file and other logging configuration options are ignored (for
# example, logging_context_format_string). (string value)
# Note: This option can be changed without restarting.
# Deprecated group/name - [DEFAULT]/log_config
#log_config_append = <None>
# Defines the format string for %%(asctime)s in log records. Default:
# %(default)s . This option is ignored if log_config_append is set. (string
# value)
#log_date_format = %Y-%m-%d %H:%M:%S
# (Optional) Name of log file to send logging output to. If no default is set,
# logging will go to stderr as defined by use_stderr. This option is ignored if
# log_config_append is set. (string value)
# Deprecated group/name - [DEFAULT]/logfile
#log_file = <None>
# (Optional) The base directory used for relative log_file paths. This option
# is ignored if log_config_append is set. (string value)
# Deprecated group/name - [DEFAULT]/logdir
#log_dir = <None>
# Uses logging handler designed to watch file system. When log file is moved or
# removed this handler will open a new log file with specified path
# instantaneously. It makes sense only if log_file option is specified and Linux
# platform is used. This option is ignored if log_config_append is set. (boolean
# value)
#watch_log_file = false
# Use syslog for logging. Existing syslog format is DEPRECATED and will be
# changed later to honor RFC5424. This option is ignored if log_config_append is
# set. (boolean value)
#use_syslog = false
# Enable journald for logging. If running in a systemd environment you may wish
# to enable journal support. Doing so will use the journal native protocol which
# includes structured metadata in addition to log messages.This option is
# ignored if log_config_append is set. (boolean value)
#use_journal = false
# Syslog facility to receive log lines. This option is ignored if
# log_config_append is set. (string value)
#syslog_log_facility = LOG_USER
# Log output to standard error. This option is ignored if log_config_append is
# set. (boolean value)
#use_stderr = false
# Format string to use for log messages with context. (string value)
#logging_context_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s
# Format string to use for log messages when context is undefined. (string
# value)
#logging_default_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s
# Additional data to append to log message when logging level for the message is
# DEBUG. (string value)
#logging_debug_format_suffix = %(funcName)s %(pathname)s:%(lineno)d
# Prefix each line of exception output with this format. (string value)
#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s
# Defines the format string for %(user_identity)s that is used in
# logging_context_format_string. (string value)
#logging_user_identity_format = %(user)s %(tenant)s %(domain)s %(user_domain)s %(project_domain)s
# List of package logging levels in logger=LEVEL pairs. This option is ignored
# if log_config_append is set. (list value)
#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,oslo_messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN,taskflow=WARN,keystoneauth=WARN,oslo.cache=INFO,dogpile.core.dogpile=INFO
# Enables or disables publication of error events. (boolean value)
#publish_errors = false
# The format for an instance that is passed with the log message. (string value)
#instance_format = "[instance: %(uuid)s] "
# The format for an instance UUID that is passed with the log message. (string
# value)
#instance_uuid_format = "[instance: %(uuid)s] "
# Interval, number of seconds, of log rate limiting. (integer value)
#rate_limit_interval = 0
# Maximum number of logged messages per rate_limit_interval. (integer value)
#rate_limit_burst = 0
# Log level name used by rate limiting: CRITICAL, ERROR, INFO, WARNING, DEBUG or
# empty string. Logs with level greater or equal to rate_limit_except_level are
# not filtered. An empty string means that all levels are filtered. (string
# value)
#rate_limit_except_level = CRITICAL
# Enables or disables fatal status of deprecations. (boolean value)
#fatal_deprecations = false

View File

@ -1,5 +1,8 @@
[DEFAULT] [DEFAULT]
output_file = etc/armada/armada.conf.sample output_file = etc/armada/armada.conf.sample
wrap_width = 80 wrap_width = 79
namespace = armada.conf namespace = armada.conf
namespace = oslo.log namespace = oslo.log
namespace = oslo.policy
namespace = oslo.middleware
namespace = keystonemiddleware.auth_token

View File

@ -0,0 +1,5 @@
[DEFAULT]
output_file = etc/armada/policy.yaml.sample
wrap_width = 79
namespace = armada

View File

@ -22,7 +22,7 @@ metadata:
data: data:
chart_name: mariadb chart_name: mariadb
release: mariadb release: mariadb
namespace: undercloud namespace: openstack
timeout: 3600 timeout: 3600
install: install:
no_hooks: false no_hooks: false
@ -44,7 +44,7 @@ metadata:
data: data:
chart_name: memcached chart_name: memcached
release: memcached release: memcached
namespace: undercloud namespace: openstack
timeout: 100 timeout: 100
install: install:
no_hooks: false no_hooks: false
@ -60,50 +60,6 @@ data:
- helm-toolkit - helm-toolkit
--- ---
schema: armada/Chart/v1 schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: etcd
data:
chart_name: etcd
release: etcd
namespace: undercloud
timeout: 3600
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git
location: git://github.com/openstack/openstack-helm
subpath: etcd
reference: master
dependencies:
- helm-toolkit
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: rabbitmq
data:
chart_name: rabbitmq
release: rabbitmq
namespace: undercloud
timeout: 100
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git
location: git://github.com/openstack/openstack-helm
subpath: rabbitmq
reference: master
dependencies:
- helm-toolkit
---
schema: armada/Chart/v1
metadata: metadata:
schema: metadata/Document/v1 schema: metadata/Document/v1
name: keystone name: keystone
@ -111,8 +67,8 @@ data:
chart_name: keystone chart_name: keystone
test: true test: true
release: keystone release: keystone
namespace: undercloud namespace: openstack
timeout: 3600 timeout: 100
install: install:
no_hooks: false no_hooks: false
upgrade: upgrade:
@ -130,13 +86,12 @@ data:
schema: armada/ChartGroup/v1 schema: armada/ChartGroup/v1
metadata: metadata:
schema: metadata/Document/v1 schema: metadata/Document/v1
name: openstack-infra-services name: keystone-infra-services
data: data:
description: "OpenStack Infra Services" description: "Keystone Infra Services"
sequenced: True sequenced: True
chart_group: chart_group:
- mariadb - mariadb
- etcd
- memcached - memcached
--- ---
schema: armada/ChartGroup/v1 schema: armada/ChartGroup/v1
@ -157,5 +112,5 @@ metadata:
data: data:
release_prefix: armada release_prefix: armada
chart_groups: chart_groups:
- openstack-infra-services - keystone-infra-services
- openstack-keystone - openstack-keystone

View File

@ -2,15 +2,17 @@ gitpython==2.1.5
grpcio==1.6.0rc1 grpcio==1.6.0rc1
grpcio-tools==1.6.0rc1 grpcio-tools==1.6.0rc1
keystoneauth1==2.21.0 keystoneauth1==2.21.0
keystonemiddleware==4.9.1
kubernetes>=1.0.0 kubernetes>=1.0.0
oslo.log==3.28.0
oslo.messaging==5.28.0
protobuf==3.2.0 protobuf==3.2.0
PyYAML==3.12 PyYAML==3.12
requests==2.17.3 requests==2.17.3
sphinx_rtd_theme sphinx_rtd_theme
supermutes==0.2.5 supermutes==0.2.5
urllib3==1.21.1 urllib3==1.21.1
uwsgi>=2.0.15
Paste>=2.0.3
PasteDeploy>=1.5.2
# API # API
falcon==1.1.0 falcon==1.1.0
@ -20,6 +22,15 @@ gunicorn==19.7.1
cliff==2.7.0 cliff==2.7.0
# Oslo # Oslo
oslo.log==3.28.0 oslo.cache>=1.5.0 # Apache-2.0
oslo.config>=3.22.0 # Apache-2.0 oslo.concurrency>=3.8.0 # Apache-2.0
oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0
oslo.context>=2.14.0 # Apache-2.0
oslo.messaging!=5.25.0,>=5.24.2 # Apache-2.0
oslo.db>=4.24.0 # Apache-2.0
oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0
oslo.log>=3.22.0 # Apache-2.0
oslo.middleware>=3.27.0 # Apache-2.0 oslo.middleware>=3.27.0 # Apache-2.0
oslo.policy>=1.23.0 # Apache-2.0
oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0
oslo.utils>=3.20.0 # Apache-2.0

View File

@ -43,6 +43,8 @@ armada =
test = armada.cli.test:TestServerCommand test = armada.cli.test:TestServerCommand
oslo.config.opts = oslo.config.opts =
armada.conf = armada.conf.opts:list_opts armada.conf = armada.conf.opts:list_opts
oslo.policy.policies =
armada = armada.common.policies:list_rules
[pbr] [pbr]
warnerrors = True warnerrors = True

6
tools/keystone-account.sh Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
openstack domain create 'ucp'
openstack project create --domain 'ucp' 'service'
openstack user create --domain ucp --project service --project-domain 'ucp' --password armada armada
openstack role add --project-domain ucp --user-domain ucp --user armada --project service admin

22
tox.ini
View File

@ -10,33 +10,37 @@ deps=
setenv= setenv=
VIRTUAL_ENV={envdir} VIRTUAL_ENV={envdir}
usedevelop = True usedevelop = True
install_command = pip install {opts} {packages}
commands = commands =
find . -type f -name "*.pyc" -delete find . -type f -name "*.pyc" -delete
python -V python -V
py.test -vvv -s --ignore=hapi py.test -vvv -s --ignore=hapi
[testenv:docs] [testenv:docs]
commands = commands =
python setup.py build_sphinx python setup.py build_sphinx
[testenv:genconfig] [testenv:genconfig]
commands = commands =
oslo-config-generator --config-file=etc/armada/config-generator.conf oslo-config-generator --config-file=etc/armada/config-generator.conf
[testenv:genpolicy]
commands =
oslopolicy-sample-generator --config-file=etc/armada/policy-generator.conf
[testenv:pep8] [testenv:pep8]
commands = commands =
flake8 {posargs} flake8 {posargs}
[testenv:bandit] [testenv:bandit]
commands = commands =
bandit -r armada -x armada/tests -n 5 bandit -r armada -x armada/tests -n 5
[testenv:coverage] [testenv:coverage]
commands = commands =
nosetests -w armada/tests/unit --cover-package=armada --with-coverage --cover-tests --exclude=.tox nosetests -w armada/tests/unit --cover-package=armada --with-coverage --cover-tests --exclude=.tox
[flake8] [flake8]
filename= *.py filename= *.py
ignore = W503,E302 ignore = W503,E302
exclude= .git, .idea, .tox, *.egg-info, *.eggs, bin, dist, hapi exclude= .git, .idea, .tox, *.egg-info, *.eggs, bin, dist, hapi