Flask to WSGI and security
This adds a lot of stuff related changing from API from Flask to WSGI and adding security. This is mostly api side. There will more changes to add testing and engine side changes. API parameter validation can also be done later. story: 2004882 Task: #29163 story: 2003844 Task: #26635 Change-Id: Id373440affd2d625933da2a6dbeb6354acd75e1e Signed-off-by: Tomi Juvonen <tomi.juvonen@nokia.com>
This commit is contained in:
parent
a890fbeeeb
commit
cc55962d52
devstack
doc/source/configuration
fenix
api
cmd
context.pyexceptions.pypolicies
policy.pyutils
workflow/workflows
@ -19,6 +19,7 @@ function configure_fenix {
|
||||
|
||||
create_service_user "$FENIX_USER_NAME" "admin"
|
||||
init_fenix_service_user_conf
|
||||
init_fenix_keystone
|
||||
|
||||
iniset $FENIX_CONF_FILE DEFAULT host "$FENIX_SERVICE_HOST"
|
||||
iniset $FENIX_API_CONF_FILE DEFAULT host "$FENIX_SERVICE_HOST"
|
||||
@ -37,6 +38,16 @@ function init_fenix_service_user_conf {
|
||||
iniset $FENIX_CONF_FILE service_user os_user_domain_name "$SERVICE_DOMAIN_NAME"
|
||||
iniset $FENIX_CONF_FILE service_user os_project_name "$SERVICE_TENANT_NAME"
|
||||
iniset $FENIX_CONF_FILE service_user os_project_domain_name "$SERVICE_DOMAIN_NAME"
|
||||
|
||||
function init_fenix_keystone {
|
||||
iniset $FENIX_API_CONF keystone_authtoken cafile "$SSL_BUNDLE_FILE"
|
||||
iniset $FENIX_API_CONF keystone_authtoken project_domain_name "$SERVICE_DOMAIN_NAME"
|
||||
iniset $FENIX_API_CONF keystone_authtoken project_name "$SERVICE_TENANT_NAME"
|
||||
iniset $FENIX_API_CONF keystone_authtoken user_domain_name "$SERVICE_DOMAIN_NAME"
|
||||
iniset $FENIX_API_CONF keystone_authtoken password "$SERVICE_PASSWORD"
|
||||
iniset $FENIX_API_CONF keystone_authtoken username "$FENIX_USER_NAME"
|
||||
iniset $FENIX_API_CONF keystone_authtoken auth_url "$KEYSTONE_SERVICE_URI"
|
||||
iniset $FENIX_API_CONF keystone_authtoken auth_type = password
|
||||
}
|
||||
|
||||
function create_fenix_accounts {
|
||||
|
@ -11,59 +11,78 @@ to the current system.
|
||||
|
||||
[DEFAULT]
|
||||
|
||||
#Mandatory configuration options
|
||||
# Mandatory configuration options
|
||||
|
||||
#Host where API is running. default="127.0.0.1"
|
||||
# Host where API is running. default="127.0.0.1"
|
||||
host = <hostname>
|
||||
#API Port. default=5000
|
||||
# API Port. default=5000
|
||||
port = <port>
|
||||
#An URL representing the messaging driver to use and its full configuration.
|
||||
# An URL representing the messaging driver to use and its full configuration.
|
||||
transport_url = <transport URL>
|
||||
|
||||
[keystone_authtoken]
|
||||
# OpenStack Identity service URL.
|
||||
auth_url = http://127.0.0.1/identity
|
||||
# Authentication type
|
||||
auth_type = password
|
||||
# PEM encoded Certificate Authority to use when verifying HTTPs connections.
|
||||
cafile = /opt/stack/data/ca-bundle.pem
|
||||
# The Fenix admin project domain.
|
||||
project_domain_name = Default
|
||||
# The Fenix admin project.
|
||||
project_name = admin
|
||||
# A domain name the os_username belongs to.
|
||||
user_domain_name = Default
|
||||
# Fenix admin user password.
|
||||
password = admin
|
||||
# Fenix user. Must have admin role.
|
||||
username = admin
|
||||
|
||||
|
||||
* Edit the ``/etc/fenix/fenix.conf`` file the configure fenix-engine
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[DEFAULT]
|
||||
|
||||
#Mandatory configuration options
|
||||
# Mandatory configuration options
|
||||
|
||||
#Host where engine is running. default="127.0.0.1"
|
||||
# Host where engine is running. default="127.0.0.1"
|
||||
host = <hostname>
|
||||
#API Port. default=5000
|
||||
# API Port. default=5000
|
||||
port = <port>
|
||||
#An URL representing the messaging driver to use and its full configuration.
|
||||
# An URL representing the messaging driver to use and its full configuration.
|
||||
transport_url = <transport URL>
|
||||
|
||||
#Optional configuration options
|
||||
# Optional configuration options
|
||||
|
||||
#Wait for project reply after message sent to project. default 120
|
||||
# Wait for project reply after message sent to project. default 120
|
||||
wait_project_reply = 120
|
||||
#Project maintenance reply confirmation time in seconds. default 40
|
||||
# Project maintenance reply confirmation time in seconds. default 40
|
||||
project_maintenance_reply = 40
|
||||
#Project scale in reply confirmation time in seconds. default 60
|
||||
# Project scale in reply confirmation time in seconds. default 60
|
||||
project_scale_in_reply = 60
|
||||
#Number of live migration retries. default 5
|
||||
# Number of live migration retries. default 5
|
||||
live_migration_retries = 5
|
||||
#How long to wait live migration to be done. default 600
|
||||
# How long to wait live migration to be done. default 600
|
||||
live_migration_wait_time = 600
|
||||
|
||||
[database]
|
||||
|
||||
#database connection URL
|
||||
# database connection URL
|
||||
connection = mysql+pymysql://fenix:FENIX_DBPASS@controller/fenix
|
||||
|
||||
[service_user]
|
||||
|
||||
#OpenStack Identity service URL. Default to environmental variable OS_AUTH_URL
|
||||
# OpenStack Identity service URL. Default to environmental variable OS_AUTH_URL
|
||||
os_auth_url = http://127.0.0.1/identity
|
||||
#Fenix user. Must have admin role. Default to environmental variable OS_USERNAME
|
||||
# Fenix user. Must have admin role. Default to environmental variable OS_USERNAME
|
||||
os_username = admin
|
||||
#Fenix admin user password. Default to environmental variable OS_PASSWORD
|
||||
# Fenix admin user password. Default to environmental variable OS_PASSWORD
|
||||
os_password = admin
|
||||
#A domain name the os_username belongs to. Default to environmental variable OS_USER_DOMAIN_NAME
|
||||
# A domain name the os_username belongs to. Default to environmental variable OS_USER_DOMAIN_NAME
|
||||
os_user_domain_name = default
|
||||
#The Fenix admin project. Default to environmental variable OS_PROJECT_NAME
|
||||
# The Fenix admin project. Default to environmental variable OS_PROJECT_NAME
|
||||
os_project_name = admin
|
||||
#The Fenix admin project domain. Default to environmental variable OS_PROJECT_DOMAIN_NAME
|
||||
# The Fenix admin project domain. Default to environmental variable OS_PROJECT_DOMAIN_NAME
|
||||
os_project_domain_name = default
|
||||
|
@ -1,52 +0,0 @@
|
||||
# Copyright (c) 2018 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 flask import Flask
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import sys
|
||||
|
||||
from fenix.api import v1
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
api_opts = [
|
||||
cfg.StrOpt('api_config',
|
||||
default="api.conf",
|
||||
help="Configuration file for API service."),
|
||||
cfg.StrOpt('host',
|
||||
default="127.0.0.1",
|
||||
help="API host IP"),
|
||||
cfg.IntOpt('port',
|
||||
default=5000,
|
||||
help="API port to use."),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(api_opts)
|
||||
logging.register_options(cfg.CONF)
|
||||
cfg.CONF(sys.argv[1:], project='fenix', prog='fenix-api')
|
||||
|
||||
|
||||
def create_app(global_config, **local_config):
|
||||
return setup_app()
|
||||
|
||||
|
||||
def setup_app(config=None):
|
||||
app = Flask(__name__, static_folder=None)
|
||||
app.config.update(PROPAGATE_EXCEPTIONS=True)
|
||||
app.register_blueprint(v1.bp, url_prefix='/v1')
|
||||
return app
|
40
fenix/api/context.py
Normal file
40
fenix/api/context.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Copyright (c) 2019 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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_serialization import jsonutils
|
||||
|
||||
import six
|
||||
|
||||
from fenix import context
|
||||
from fenix import exceptions
|
||||
|
||||
|
||||
def ctx_from_headers(headers):
|
||||
try:
|
||||
service_catalog = jsonutils.loads(headers['X-Service-Catalog'])
|
||||
except KeyError:
|
||||
raise exceptions.ServiceCatalogNotFound()
|
||||
except TypeError:
|
||||
raise exceptions.WrongFormat()
|
||||
|
||||
return context.FenixContext(
|
||||
user_id=headers['X-User-Id'],
|
||||
project_id=headers['X-Project-Id'],
|
||||
auth_token=headers['X-Auth-Token'],
|
||||
service_catalog=service_catalog,
|
||||
user_name=headers['X-User-Name'],
|
||||
project_name=headers['X-Project-Name'],
|
||||
roles=list(map(six.text_type.strip, headers['X-Roles'].split(','))),
|
||||
)
|
44
fenix/api/root.py
Normal file
44
fenix/api/root.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Copyright (c) 2019 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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_serialization import jsonutils
|
||||
import pecan
|
||||
|
||||
from fenix.api.v1 import controllers
|
||||
|
||||
|
||||
class RootController(object):
|
||||
|
||||
v1 = controllers.V1Controller()
|
||||
|
||||
def _append_versions_from_controller(self, versions, controller, path):
|
||||
for version in getattr(controller, 'versions', None):
|
||||
version['links'] = [{
|
||||
"href": "{0}/{1}".format(pecan.request.host_url, path),
|
||||
"rel": "self"}]
|
||||
versions.append(version)
|
||||
|
||||
@pecan.expose(content_type='application/json')
|
||||
def index(self):
|
||||
pecan.response.status_code = 300
|
||||
pecan.response.content_type = 'application/json'
|
||||
versions = {"versions": []}
|
||||
self._append_versions_from_controller(versions['versions'],
|
||||
self.v1, 'v1')
|
||||
return jsonutils.dump_as_bytes(versions)
|
||||
|
||||
@pecan.expose(content_type='application/json')
|
||||
def versions(self):
|
||||
return self.index()
|
@ -1,26 +0,0 @@
|
||||
# Copyright (c) 2018 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 flask import Blueprint
|
||||
import flask_restful as restful
|
||||
|
||||
from fenix.api.v1.routes import routes
|
||||
|
||||
|
||||
bp = Blueprint('v1', __name__)
|
||||
api = restful.Api(bp, catch_all_404s=True)
|
||||
|
||||
for route in routes:
|
||||
api.add_resource(route.pop('resource'), *route.pop('urls'), **route)
|
81
fenix/api/v1/app.py
Normal file
81
fenix/api/v1/app.py
Normal file
@ -0,0 +1,81 @@
|
||||
# Copyright (c) 2018 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 sys
|
||||
|
||||
from keystonemiddleware import auth_token
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import pecan
|
||||
|
||||
from fenix.api.v1 import hooks
|
||||
from fenix.api.v1 import middleware
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
api_opts = [
|
||||
cfg.StrOpt('api_config',
|
||||
default="api.conf",
|
||||
help="Configuration file for API service."),
|
||||
cfg.StrOpt('host',
|
||||
default="127.0.0.1",
|
||||
help="API host IP"),
|
||||
cfg.IntOpt('port',
|
||||
default=5000,
|
||||
help="API port to use."),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(api_opts)
|
||||
logging.register_options(cfg.CONF)
|
||||
cfg.CONF(sys.argv[1:], project='fenix', prog='fenix-api')
|
||||
|
||||
|
||||
def setup_app(pecan_config=None, debug=False, argv=None):
|
||||
|
||||
app_hooks = [hooks.ConfigHook(),
|
||||
hooks.DBHook(),
|
||||
hooks.ContextHook(),
|
||||
hooks.RPCHook()]
|
||||
|
||||
app = pecan.make_app(pecan_config.app.root,
|
||||
debug=CONF.debug,
|
||||
hooks=app_hooks,
|
||||
wrap_app=middleware.ParsableErrorMiddleware,
|
||||
guess_content_type_from_ext=False)
|
||||
|
||||
# WSGI middleware for debugging
|
||||
# if CONF.log_exchange:
|
||||
# app = debug.Debug.factory(pecan_config)(app)
|
||||
|
||||
# WSGI middleware for Keystone auth
|
||||
# NOTE(sbauza): ACLs are always active unless for unittesting where
|
||||
# enable_acl could be set to False
|
||||
if pecan_config.app.enable_acl:
|
||||
app = auth_token.AuthProtocol(app, {})
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def make_app():
|
||||
config = {
|
||||
'app': {
|
||||
'modules': ['fenix.api.v1'],
|
||||
'root': 'fenix.api.root.RootController',
|
||||
'enable_acl': True,
|
||||
}
|
||||
}
|
||||
app = pecan.load_app(config)
|
||||
return app
|
@ -1,52 +0,0 @@
|
||||
# Copyright (c) 2018 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 decorator
|
||||
import flask
|
||||
import flask_restful as restful
|
||||
import inspect
|
||||
import json
|
||||
import re
|
||||
|
||||
SORT_KEY_SPLITTER = re.compile('[ ,]')
|
||||
|
||||
|
||||
class Resource(restful.Resource):
|
||||
method_decorators = []
|
||||
|
||||
def error_response(self, status_code, message):
|
||||
body = json.dumps(
|
||||
{
|
||||
'status': status_code,
|
||||
'message': message
|
||||
},
|
||||
)
|
||||
resp = flask.make_response("{body}\n".format(body=body))
|
||||
resp.status_code = status_code
|
||||
return resp
|
||||
|
||||
|
||||
@decorator.decorator
|
||||
def http_codes(f, *args, **kwargs):
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except Exception as err:
|
||||
try:
|
||||
inspect.getmodule(f).LOG.error(
|
||||
'Error during %s: %s' % (f.__qualname__, err))
|
||||
except AttributeError:
|
||||
inspect.getmodule(f).LOG.error(
|
||||
'Error during %s: %s' % (f.__name__, err))
|
||||
return args[0].error_response(500, 'Unknown Error')
|
64
fenix/api/v1/controllers/__init__.py
Normal file
64
fenix/api/v1/controllers/__init__.py
Normal file
@ -0,0 +1,64 @@
|
||||
# Copyright (c) 2019 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Version 1 of the API.
|
||||
"""
|
||||
|
||||
from oslo_log import log as logging
|
||||
import pecan
|
||||
from pecan import rest
|
||||
|
||||
from fenix.api.v1.controllers import maintenance as controller
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class V1Controller(rest.RestController):
|
||||
|
||||
versions = [{"id": "v1", "status": "CURRENT"}]
|
||||
_routes = {}
|
||||
|
||||
# maps to v1/maintenance
|
||||
maintenance = controller.MaintenanceController()
|
||||
# maps to v1/maintenance/{session_id}
|
||||
session = controller.SessionController()
|
||||
# maps to v1/maintenance/{session_id}/{project_id}
|
||||
project = controller.ProjectController()
|
||||
|
||||
@pecan.expose()
|
||||
def _route(self, args):
|
||||
"""Overrides the default routing behavior.
|
||||
|
||||
It allows to map controller URL with correct controller instance.
|
||||
By default, it maps with the same name.
|
||||
"""
|
||||
|
||||
try:
|
||||
route = self._routes.get(args[0], args[0])
|
||||
depth = len(args)
|
||||
if route is None:
|
||||
# NOTE(sbauza): Route must map to a non-existing controller
|
||||
args[0] = 'http404-nonexistingcontroller'
|
||||
elif depth == 1:
|
||||
args[0] = route
|
||||
elif depth == 2 and route == "maintenance":
|
||||
args[0] = "session"
|
||||
elif depth == 3 and route == "maintenance":
|
||||
args[0] = "project"
|
||||
else:
|
||||
args[0] = 'http404-nonexistingcontroller'
|
||||
except IndexError:
|
||||
LOG.error("No args found on V1 controller")
|
||||
return super(V1Controller, self)._route(args)
|
127
fenix/api/v1/controllers/maintenance.py
Normal file
127
fenix/api/v1/controllers/maintenance.py
Normal file
@ -0,0 +1,127 @@
|
||||
# Copyright (c) 2019 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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
|
||||
from pecan import abort
|
||||
from pecan import expose
|
||||
from pecan import request
|
||||
from pecan import response
|
||||
from pecan import rest
|
||||
|
||||
from oslo_log import log
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from fenix.api.v1 import maintenance
|
||||
from fenix import policy
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectController(rest.RestController):
|
||||
|
||||
name = 'project'
|
||||
|
||||
def __init__(self):
|
||||
self.engine_rpcapi = maintenance.EngineRPCAPI()
|
||||
|
||||
# GET /v1/maintenance/<session_id>/<project_id>
|
||||
@policy.authorize('maintenance:session:project', 'get')
|
||||
@expose(content_type='application/json')
|
||||
def get(self, session_id, project_id):
|
||||
if request.body:
|
||||
LOG.error("Unexpected data")
|
||||
abort(400)
|
||||
engine_data = self.engine_rpcapi.project_get_session(session_id,
|
||||
project_id)
|
||||
response.body = jsonutils.dumps(engine_data)
|
||||
|
||||
# PUT /v1/maintenance/<session_id>/<project_id>
|
||||
@policy.authorize('maintenance:session:project', 'put')
|
||||
@expose(content_type='application/json')
|
||||
def put(self, session_id, project_id):
|
||||
data = json.loads(request.body.decode('utf8'))
|
||||
engine_data = self.engine_rpcapi.project_update_session(session_id,
|
||||
project_id,
|
||||
data)
|
||||
response.body = jsonutils.dumps(engine_data)
|
||||
|
||||
|
||||
class SessionController(rest.RestController):
|
||||
|
||||
name = 'session'
|
||||
|
||||
def __init__(self):
|
||||
self.engine_rpcapi = maintenance.EngineRPCAPI()
|
||||
|
||||
# GET /v1/maintenance/<session_id>
|
||||
@policy.authorize('maintenance:session', 'get')
|
||||
@expose(content_type='application/json')
|
||||
def get(self, session_id):
|
||||
if request.body:
|
||||
LOG.error("Unexpected data")
|
||||
abort(400)
|
||||
session = self.engine_rpcapi.admin_get_session(session_id)
|
||||
if session is None:
|
||||
response.status = 404
|
||||
return {"error": "Invalid session"}
|
||||
response.body = jsonutils.dumps(session)
|
||||
|
||||
# PUT /v1/maintenance/<session_id>
|
||||
@policy.authorize('maintenance:session', 'put')
|
||||
@expose(content_type='application/json')
|
||||
def put(self, session_id):
|
||||
data = json.loads(request.body.decode('utf8'))
|
||||
engine_data = self.engine_rpcapi.admin_update_session(session_id, data)
|
||||
response.body = jsonutils.dumps(engine_data)
|
||||
|
||||
# DELETE /v1/maintenance/<session_id>
|
||||
@policy.authorize('maintenance:session', 'delete')
|
||||
@expose(content_type='application/json')
|
||||
def delete(self, session_id):
|
||||
if request.body:
|
||||
LOG.error("Unexpected data")
|
||||
abort(400)
|
||||
engine_data = self.engine_rpcapi.admin_delete_session(session_id)
|
||||
response.body = jsonutils.dumps(engine_data)
|
||||
|
||||
|
||||
class MaintenanceController(rest.RestController):
|
||||
|
||||
name = 'maintenance'
|
||||
|
||||
def __init__(self):
|
||||
self.engine_rpcapi = maintenance.EngineRPCAPI()
|
||||
|
||||
# GET /v1/maintenance
|
||||
@policy.authorize('maintenance', 'get')
|
||||
@expose(content_type='application/json')
|
||||
def get(self):
|
||||
if request.body:
|
||||
LOG.error("Unexpected data")
|
||||
abort(400)
|
||||
sessions = self.engine_rpcapi.admin_get()
|
||||
response.body = jsonutils.dumps(sessions)
|
||||
|
||||
# POST /v1/maintenance
|
||||
@policy.authorize('maintenance', 'post')
|
||||
@expose(content_type='application/json')
|
||||
def post(self):
|
||||
LOG.debug("POST /v1/maintenance")
|
||||
data = json.loads(request.body.decode('utf8'))
|
||||
session = self.engine_rpcapi.admin_create_session(data)
|
||||
if session is None:
|
||||
response.status = 509
|
||||
return {"error": "Too many sessions"}
|
||||
response.body = jsonutils.dumps(session)
|
68
fenix/api/v1/hooks.py
Normal file
68
fenix/api/v1/hooks.py
Normal file
@ -0,0 +1,68 @@
|
||||
# Copyright (c) 2019 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from pecan import hooks
|
||||
|
||||
from fenix.api import context
|
||||
from fenix.api.v1 import maintenance
|
||||
|
||||
from fenix.db import api as dbapi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigHook(hooks.PecanHook):
|
||||
"""ConfigHook
|
||||
|
||||
Attach the configuration object to the request
|
||||
so controllers can get to it.
|
||||
"""
|
||||
|
||||
def before(self, state):
|
||||
state.request.cfg = cfg.CONF
|
||||
|
||||
|
||||
class DBHook(hooks.PecanHook):
|
||||
"""Attach the dbapi object to the request so controllers can get to it."""
|
||||
|
||||
def before(self, state):
|
||||
state.request.dbapi = dbapi.get_instance()
|
||||
|
||||
|
||||
class ContextHook(hooks.PecanHook):
|
||||
"""Configures a request context and attaches it to the request."""
|
||||
|
||||
def before(self, state):
|
||||
state.request.context = context.ctx_from_headers(state.request.headers)
|
||||
state.request.context.__enter__()
|
||||
|
||||
# NOTE(sbauza): on_error() can be fired before after() if the original
|
||||
# exception is not catched by WSME. That's necessary to not
|
||||
# handle context.__exit__() within on_error() as it could
|
||||
# lead to pop the stack twice for the same request
|
||||
def after(self, state):
|
||||
# If no API extensions are loaded, context is empty
|
||||
if state.request.context:
|
||||
state.request.context.__exit__(None, None, None)
|
||||
|
||||
|
||||
class RPCHook(hooks.PecanHook):
|
||||
"""Attach the rpcapi object to the request so controllers can get to it."""
|
||||
|
||||
def before(self, state):
|
||||
state.request.rpcapi = maintenance.EngineRPCAPI()
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2018 OpenStack Foundation.
|
||||
# Copyright (c) 2019 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -13,12 +13,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from flask import request
|
||||
import json
|
||||
from oslo_log import log
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from fenix.api.v1 import base
|
||||
from fenix import engine
|
||||
from fenix.utils import service
|
||||
|
||||
@ -48,9 +44,10 @@ class EngineRPCAPI(service.RPCClient):
|
||||
"""Delete maintenance workflow session thread"""
|
||||
return self.call('admin_delete_session', session_id=session_id)
|
||||
|
||||
def admin_update_session(self, session_id):
|
||||
def admin_update_session(self, session_id, data):
|
||||
"""Update maintenance workflow session"""
|
||||
return self.call('admin_update_session', session_id=session_id)
|
||||
return self.call('admin_update_session', session_id=session_id,
|
||||
data=data)
|
||||
|
||||
def project_get_session(self, session_id, project_id):
|
||||
"""Get maintenance workflow session project specific details"""
|
||||
@ -61,95 +58,3 @@ class EngineRPCAPI(service.RPCClient):
|
||||
"""Update maintenance workflow session project state"""
|
||||
return self.call('project_update_session', session_id=session_id,
|
||||
project_id=project_id, data=data)
|
||||
|
||||
|
||||
class Maintenance(base.Resource):
|
||||
|
||||
def __init__(self):
|
||||
self.engine_rpcapi = EngineRPCAPI()
|
||||
|
||||
@base.http_codes
|
||||
def get(self):
|
||||
if request.data:
|
||||
LOG.error("Unexpected data")
|
||||
return {}, 400, None
|
||||
LOG.info("admin get")
|
||||
LOG.info("self.engine_rpcapi.admin_get")
|
||||
sessions = self.engine_rpcapi.admin_get()
|
||||
LOG.info("return: %s" % sessions)
|
||||
return jsonutils.to_primitive(sessions), 200, None
|
||||
|
||||
@base.http_codes
|
||||
def post(self):
|
||||
LOG.info("admin post: first")
|
||||
data = json.loads(request.data.decode('utf8'))
|
||||
LOG.info("admin post: %s" % data)
|
||||
session = self.engine_rpcapi.admin_create_session(data)
|
||||
if session is None:
|
||||
return {"error": "Too many sessions"}, 509, None
|
||||
LOG.info("return: %s" % session)
|
||||
return jsonutils.to_primitive(session), 200, None
|
||||
|
||||
|
||||
class MaintenanceSession(base.Resource):
|
||||
|
||||
def __init__(self):
|
||||
self.engine_rpcapi = EngineRPCAPI()
|
||||
|
||||
@base.http_codes
|
||||
def get(self, session_id=None):
|
||||
if request.data:
|
||||
LOG.error("Unexpected data")
|
||||
return {}, 400, None
|
||||
LOG.info("admin session_id get")
|
||||
session = self.engine_rpcapi.admin_get_session(session_id)
|
||||
if session is None:
|
||||
return {"error": "Invalid session"}, 404, None
|
||||
return jsonutils.to_primitive(session), 200, None
|
||||
|
||||
@base.http_codes
|
||||
def put(self, session_id=None):
|
||||
data = json.loads(request.data.decode('utf8'))
|
||||
LOG.info("admin session_id put: %s" % data)
|
||||
engine_data = self.engine_rpcapi.admin_update_session(session_id)
|
||||
LOG.info("engine_data: %s" % engine_data)
|
||||
response_body = {'maintenance': request.base_url,
|
||||
'session_id': session_id}
|
||||
return jsonutils.to_primitive(response_body), 200, None
|
||||
|
||||
@base.http_codes
|
||||
def delete(self, session_id=None):
|
||||
if request.data:
|
||||
LOG.error("Unexpected data")
|
||||
return {}, 400, None
|
||||
LOG.info("admin session_id delete")
|
||||
ret = self.engine_rpcapi.admin_delete_session(session_id)
|
||||
LOG.info("return: %s" % ret)
|
||||
return jsonutils.to_primitive(ret), 200, None
|
||||
|
||||
|
||||
class MaintenanceSessionProject(base.Resource):
|
||||
|
||||
def __init__(self):
|
||||
self.engine_rpcapi = EngineRPCAPI()
|
||||
|
||||
@base.http_codes
|
||||
def get(self, session_id=None, project_id=None):
|
||||
if request.data:
|
||||
LOG.error("Unexpected data")
|
||||
return {}, 400, None
|
||||
LOG.info("%s_get" % project_id)
|
||||
engine_data = self.engine_rpcapi.project_get_session(session_id,
|
||||
project_id)
|
||||
LOG.info("engine_data: %s" % engine_data)
|
||||
return jsonutils.to_primitive(engine_data), 200, None
|
||||
|
||||
@base.http_codes
|
||||
def put(self, session_id=None, project_id=None):
|
||||
data = json.loads(request.data.decode('utf8'))
|
||||
LOG.info("%s_put: %s" % (project_id, data))
|
||||
engine_data = self.engine_rpcapi.project_update_session(session_id,
|
||||
project_id,
|
||||
data)
|
||||
LOG.info("engine_data: %s" % engine_data)
|
||||
return jsonutils.to_primitive(engine_data), 200, None
|
||||
|
137
fenix/api/v1/middleware/__init__.py
Normal file
137
fenix/api/v1/middleware/__init__.py
Normal file
@ -0,0 +1,137 @@
|
||||
# Copyright (c) 2014 Bull.
|
||||
#
|
||||
# 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_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
import webob
|
||||
|
||||
from fenix.db import exceptions as db_exceptions
|
||||
from fenix import exceptions
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ParsableErrorMiddleware(object):
|
||||
"""Middleware which prepared body to the client
|
||||
|
||||
Middleware to replace the plain text message body of an error
|
||||
response with one formatted so the client can parse it.
|
||||
Based on pecan.middleware.errordocument
|
||||
"""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
# Request for this state, modified by replace_start_response()
|
||||
# and used when an error is being reported.
|
||||
state = {}
|
||||
faultstring = None
|
||||
|
||||
def replacement_start_response(status, headers, exc_info=None):
|
||||
"""Overrides the default response to make errors parsable."""
|
||||
try:
|
||||
status_code = int(status.split(' ')[0])
|
||||
except (ValueError, TypeError): # pragma: nocover
|
||||
raise exceptions.FenixException(_(
|
||||
'Status {0} was unexpected').format(status))
|
||||
else:
|
||||
if status_code >= 400:
|
||||
# Remove some headers so we can replace them later
|
||||
# when we have the full error message and can
|
||||
# compute the length.
|
||||
headers = [(h, v)
|
||||
for (h, v) in headers
|
||||
if h.lower() != 'content-length'
|
||||
]
|
||||
# Save the headers as we need to modify them.
|
||||
state['status_code'] = status_code
|
||||
state['headers'] = headers
|
||||
state['exc_info'] = exc_info
|
||||
return start_response(status, headers, exc_info)
|
||||
|
||||
# NOTE(sbauza): As agreed, XML is not supported with API v2, but can
|
||||
# still work if no errors are raised
|
||||
try:
|
||||
app_iter = self.app(environ, replacement_start_response)
|
||||
except exceptions.FenixException as e:
|
||||
faultstring = "{0} {1}".format(e.__class__.__name__, str(e))
|
||||
replacement_start_response(
|
||||
webob.response.Response(status=str(e.code)).status,
|
||||
[('Content-Type', 'application/json; charset=UTF-8')]
|
||||
)
|
||||
else:
|
||||
if not state:
|
||||
return app_iter
|
||||
try:
|
||||
res_dct = jsonutils.loads(app_iter[0])
|
||||
except ValueError:
|
||||
return app_iter
|
||||
else:
|
||||
try:
|
||||
faultstring = res_dct['faultstring']
|
||||
except KeyError:
|
||||
return app_iter
|
||||
|
||||
traceback_marker = 'Traceback (most recent call last):'
|
||||
remote_marker = 'Remote error: '
|
||||
|
||||
if not faultstring:
|
||||
return app_iter
|
||||
|
||||
if traceback_marker in faultstring:
|
||||
# Cut-off traceback.
|
||||
faultstring = faultstring.split(traceback_marker, 1)[0]
|
||||
faultstring = faultstring.split('[u\'', 1)[0]
|
||||
if remote_marker in faultstring:
|
||||
# RPC calls put that string on
|
||||
try:
|
||||
faultstring = faultstring.split(
|
||||
remote_marker, 1)[1]
|
||||
except IndexError:
|
||||
pass
|
||||
faultstring = faultstring.rstrip()
|
||||
|
||||
try:
|
||||
(exc_name, exc_value) = faultstring.split(' ', 1)
|
||||
except (ValueError, AttributeError):
|
||||
LOG.warning('Incorrect Remote error {0}'.format(faultstring))
|
||||
else:
|
||||
cls = getattr(exceptions, exc_name, None)
|
||||
if cls is not None:
|
||||
faultstring = str(cls(exc_value))
|
||||
state['status_code'] = cls.code
|
||||
else:
|
||||
# Get the exception from db exceptions and hide
|
||||
# the message because could contain table/column
|
||||
# information
|
||||
cls = getattr(db_exceptions, exc_name, None)
|
||||
if cls is not None:
|
||||
faultstring = '{0}: A database error occurred'.format(
|
||||
cls.__name__)
|
||||
state['status_code'] = cls.code
|
||||
|
||||
# NOTE(sbauza): Client expects a JSON encoded dict
|
||||
body = [jsonutils.dump_as_bytes(
|
||||
{'error_code': state['status_code'],
|
||||
'error_message': faultstring,
|
||||
'error_name': state['status_code']}
|
||||
)]
|
||||
start_response(
|
||||
webob.response.Response(status=state['status_code']).status,
|
||||
state['headers'],
|
||||
state['exc_info']
|
||||
)
|
||||
return body
|
@ -13,22 +13,24 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import gettext
|
||||
from wsgiref import simple_server
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from fenix import api
|
||||
gettext.install('fenix')
|
||||
|
||||
from fenix.api.v1 import app as v1_app
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def main():
|
||||
app = api.setup_app()
|
||||
host, port = cfg.CONF.host, cfg.CONF.port
|
||||
LOG.info("host %s port %s" % (host, port))
|
||||
srv = simple_server.make_server(host, port, app)
|
||||
logging.setup(cfg.CONF, 'fenix')
|
||||
app = v1_app.make_app()
|
||||
srv = simple_server.make_server(cfg.CONF.host, cfg.CONF.port, app)
|
||||
LOG.info("fenix-api started")
|
||||
srv.serve_forever()
|
||||
|
||||
|
@ -100,3 +100,7 @@ class FenixContext(BaseContext):
|
||||
|
||||
def current():
|
||||
return FenixContext.current()
|
||||
|
||||
|
||||
def elevated():
|
||||
return FenixContext.elevated()
|
||||
|
@ -47,3 +47,26 @@ class FenixException(Exception):
|
||||
message = self.msg_fmt
|
||||
|
||||
super(FenixException, self).__init__(message)
|
||||
|
||||
|
||||
class NotFound(FenixException):
|
||||
"""Object not found exception."""
|
||||
msg_fmt = "Object with %(object)s not found"
|
||||
code = 404
|
||||
|
||||
|
||||
class NotAuthorized(FenixException):
|
||||
msg_fmt = "Not authorized"
|
||||
code = 403
|
||||
|
||||
|
||||
class PolicyNotAuthorized(NotAuthorized):
|
||||
msg_fmt = "Policy doesn't allow %(action)s to be performed"
|
||||
|
||||
|
||||
class ServiceCatalogNotFound(NotFound):
|
||||
msg_fmt = "Could not find service catalog"
|
||||
|
||||
|
||||
class WrongFormat(FenixException):
|
||||
msg_fmt = "Unenxpectable object format"
|
||||
|
27
fenix/policies/__init__.py
Normal file
27
fenix/policies/__init__.py
Normal file
@ -0,0 +1,27 @@
|
||||
# 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 fenix.policies import base
|
||||
from fenix.policies import maintenance
|
||||
from fenix.policies import project
|
||||
from fenix.policies import session
|
||||
|
||||
|
||||
def list_rules():
|
||||
return itertools.chain(
|
||||
base.list_rules(),
|
||||
maintenance.list_rules(),
|
||||
session.list_rules(),
|
||||
project.list_rules(),
|
||||
)
|
@ -1,6 +1,3 @@
|
||||
# Copyright (c) 2018 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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
|
||||
@ -13,16 +10,23 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from fenix.api.v1 import maintenance
|
||||
from oslo_policy import policy
|
||||
|
||||
routes = [
|
||||
dict(resource=maintenance.Maintenance,
|
||||
urls=['/maintenance'],
|
||||
endpoint='maintenance'),
|
||||
dict(resource=maintenance.MaintenanceSession,
|
||||
urls=['/maintenance/<session_id>'],
|
||||
endpoint='maintenance/<session_id>'),
|
||||
dict(resource=maintenance.MaintenanceSessionProject,
|
||||
urls=['/maintenance/<session_id>/<project_id>'],
|
||||
endpoint='maintenance/<session_id>/<project_id>'),
|
||||
RULE_ADMIN = 'rule:admin'
|
||||
RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner'
|
||||
RULE_ANY = '@'
|
||||
|
||||
rules = [
|
||||
policy.RuleDefault(
|
||||
name="admin",
|
||||
check_str="is_admin:True or role:admin",
|
||||
description="Default rule for most Admin APIs."),
|
||||
policy.RuleDefault(
|
||||
name="admin_or_owner",
|
||||
check_str="rule:admin or project_id:%(project_id)s",
|
||||
description="Default rule for most non-Admin APIs.")
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return rules
|
49
fenix/policies/maintenance.py
Normal file
49
fenix/policies/maintenance.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2019 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 fenix.policies import base
|
||||
|
||||
POLICY_ROOT = 'fenix:maintenance:%s'
|
||||
|
||||
maintenance_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name=POLICY_ROOT % 'get',
|
||||
check_str=base.RULE_ADMIN,
|
||||
description='Policy rule for listing maintenance sessions API',
|
||||
operations=[
|
||||
{
|
||||
'path': '/{api_version}/maintenance',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=POLICY_ROOT % 'post',
|
||||
check_str=base.RULE_ADMIN,
|
||||
description='Policy rule for creating maintenance session API',
|
||||
operations=[
|
||||
{
|
||||
'path': '/{api_version}/maintenance',
|
||||
'method': 'POST'
|
||||
}
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return maintenance_policies
|
50
fenix/policies/project.py
Normal file
50
fenix/policies/project.py
Normal file
@ -0,0 +1,50 @@
|
||||
# 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 fenix.policies import base
|
||||
|
||||
POLICY_ROOT = 'fenix:maintenance:session:project:%s'
|
||||
|
||||
project_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name=POLICY_ROOT % 'get',
|
||||
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||
description='Policy rule for showing project session affected '
|
||||
'instances API.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/{api_version}/maintenance/{seission_id}/'
|
||||
'{project_id}',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=POLICY_ROOT % 'put',
|
||||
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||
description='Policy rule for setting project session affected '
|
||||
'instances actions API.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/{api_version}/maintenance/{seission_id}/'
|
||||
'{project_id}',
|
||||
'method': 'PUT'
|
||||
}
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return project_policies
|
57
fenix/policies/session.py
Normal file
57
fenix/policies/session.py
Normal 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.
|
||||
|
||||
from oslo_policy import policy
|
||||
|
||||
from fenix.policies import base
|
||||
|
||||
POLICY_ROOT = 'fenix:maintenance:session:%s'
|
||||
|
||||
session_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name=POLICY_ROOT % 'get',
|
||||
check_str=base.RULE_ADMIN,
|
||||
description='Policy rule for showing maintenance session API',
|
||||
operations=[
|
||||
{
|
||||
'path': '/{api_version}/maintenance/{seission_id}',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=POLICY_ROOT % 'put',
|
||||
check_str=base.RULE_ADMIN,
|
||||
description='Policy rule for updating maintenance session API',
|
||||
operations=[
|
||||
{
|
||||
'path': '/{api_version}/maintenance/{seission_id}',
|
||||
'method': 'PUT'
|
||||
}
|
||||
]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=POLICY_ROOT % 'delete',
|
||||
check_str=base.RULE_ADMIN,
|
||||
description='Policy rule for deleting maintenance session API',
|
||||
operations=[
|
||||
{
|
||||
'path': '/{api_version}/maintenance/{seission_id}',
|
||||
'method': 'DELETE'
|
||||
}
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return session_policies
|
110
fenix/policy.py
Normal file
110
fenix/policy.py
Normal file
@ -0,0 +1,110 @@
|
||||
# Copyright (c) 2013 Bull.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Policy Engine For Fenix."""
|
||||
|
||||
import functools
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_policy import opts
|
||||
from oslo_policy import policy
|
||||
|
||||
from fenix import context
|
||||
from fenix import exceptions
|
||||
from fenix import policies
|
||||
|
||||
CONF = cfg.CONF
|
||||
opts.set_defaults(CONF)
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
_ENFORCER = None
|
||||
|
||||
|
||||
def reset():
|
||||
global _ENFORCER
|
||||
|
||||
if _ENFORCER:
|
||||
_ENFORCER.clear()
|
||||
_ENFORCER = None
|
||||
|
||||
|
||||
def init():
|
||||
global _ENFORCER
|
||||
if not _ENFORCER:
|
||||
_ENFORCER = policy.Enforcer(CONF)
|
||||
_ENFORCER.register_defaults(policies.list_rules())
|
||||
|
||||
|
||||
def set_rules(data, default_rule=None):
|
||||
default_rule = default_rule or CONF.policy_default_rule
|
||||
if not _ENFORCER:
|
||||
init()
|
||||
if default_rule:
|
||||
_ENFORCER.default_rule = default_rule
|
||||
_ENFORCER.set_rules(policy.Rules.load(data, default_rule))
|
||||
|
||||
|
||||
def enforce(context, action, target, do_raise=True):
|
||||
"""Verifies that the action is valid on the target in this context.
|
||||
|
||||
:param context: fenix context
|
||||
: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}``
|
||||
:param do_raise: if True (the default), raises PolicyNotAuthorized;
|
||||
if False, returns False
|
||||
:raises fenix.exceptions.PolicyNotAuthorized: if verification fails
|
||||
and do_raise is True.
|
||||
:return: returns a non-False value (not necessarily "True") if
|
||||
authorized, and the exact value False if not authorized and
|
||||
do_raise is False.
|
||||
"""
|
||||
|
||||
init()
|
||||
|
||||
credentials = context.to_dict()
|
||||
LOG.debug("enforce %s" % credentials)
|
||||
|
||||
# Add the exceptions arguments if asked to do a raise
|
||||
extra = {}
|
||||
if do_raise:
|
||||
extra.update(exc=exceptions.PolicyNotAuthorized, action=action)
|
||||
|
||||
return _ENFORCER.enforce(action, target, credentials, do_raise=do_raise,
|
||||
**extra)
|
||||
|
||||
|
||||
def authorize(extension, action=None, api='fenix', ctx=None,
|
||||
target=None):
|
||||
def decorator(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
cur_ctx = ctx or context.current()
|
||||
tgt = target or {'project_id': cur_ctx.project_id,
|
||||
'user_id': cur_ctx.user_id}
|
||||
LOG.debug("authorize target: %s" % tgt)
|
||||
if action is None:
|
||||
act = '%s:%s' % (api, extension)
|
||||
else:
|
||||
act = '%s:%s:%s' % (api, extension, action)
|
||||
LOG.debug("authorize policy: %s" % act)
|
||||
enforce(cur_ctx, act, tgt)
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
return decorator
|
@ -102,7 +102,7 @@ class EngineEndpoint(object):
|
||||
def admin_get(self, ctx):
|
||||
"""Get maintenance workflow sessions"""
|
||||
LOG.info("EngineEndpoint: admin_get")
|
||||
return {'sessions': self.workflow_sessions.keys()}
|
||||
return {"sessions": self.workflow_sessions.keys()}
|
||||
|
||||
def admin_create_session(self, ctx, data):
|
||||
"""Create maintenance workflow session thread"""
|
||||
@ -145,14 +145,14 @@ class EngineEndpoint(object):
|
||||
(self.session_id, data["workflow"]))
|
||||
|
||||
self.workflow_sessions[session_id].start()
|
||||
return {'session_id': session_id}
|
||||
return {"session_id": session_id}
|
||||
|
||||
def admin_get_session(self, ctx, session_id):
|
||||
"""Get maintenance workflow session details"""
|
||||
if not self._validate_session(session_id):
|
||||
return None
|
||||
LOG.info("EngineEndpoint: admin_get_session")
|
||||
return ({'session_id': session_id, 'state':
|
||||
return ({"session_id": session_id, "state":
|
||||
self.workflow_sessions[session_id].session.state})
|
||||
|
||||
def admin_delete_session(self, ctx, session_id):
|
||||
@ -165,10 +165,11 @@ class EngineEndpoint(object):
|
||||
rmtree(session_dir)
|
||||
return {}
|
||||
|
||||
def admin_update_session(self, ctx, session_id):
|
||||
def admin_update_session(self, ctx, session_id, data):
|
||||
"""Update maintenance workflow session"""
|
||||
LOG.info("EngineEndpoint: admin_update_session")
|
||||
return {'session_id': session_id}
|
||||
# TBD Update data to workflow and return updated data
|
||||
return data
|
||||
|
||||
def project_get_session(self, ctx, session_id, project_id):
|
||||
"""Get maintenance workflow session project specific details"""
|
||||
@ -177,7 +178,7 @@ class EngineEndpoint(object):
|
||||
LOG.info("EngineEndpoint: project_get_session")
|
||||
instance_ids = (self.workflow_sessions[session_id].
|
||||
state_instance_ids(project_id))
|
||||
return {'instance_ids': instance_ids}
|
||||
return {"instance_ids": instance_ids}
|
||||
|
||||
def project_update_session(self, ctx, session_id, project_id, data):
|
||||
"""Update maintenance workflow session project state"""
|
||||
@ -185,9 +186,9 @@ class EngineEndpoint(object):
|
||||
session_obj = self.workflow_sessions[session_id]
|
||||
project = session_obj.project(project_id)
|
||||
project.state = data["state"]
|
||||
if 'instance_actions' in data:
|
||||
if "instance_actions" in data:
|
||||
session_obj.proj_instance_actions[project_id] = (
|
||||
data['instance_actions'].copy())
|
||||
data["instance_actions"].copy())
|
||||
return data
|
||||
|
||||
|
||||
|
@ -887,11 +887,13 @@ class Workflow(BaseWorkflow):
|
||||
self.disable_host_nova_compute(compute)
|
||||
for host in self.get_controller_hosts():
|
||||
LOG.info('IN_MAINTENANCE controller %s' % host)
|
||||
self._admin_notify(self.conf.workflow_project, host,
|
||||
self._admin_notify(self.conf.service_user.os_project_name,
|
||||
host,
|
||||
'IN_MAINTENANCE',
|
||||
self.session_id)
|
||||
self.host_maintenance(host)
|
||||
self._admin_notify(self.conf.workflow_project, host,
|
||||
self._admin_notify(self.conf.service_user.os_project_name,
|
||||
host,
|
||||
'MAINTENANCE_COMPLETE',
|
||||
self.session_id)
|
||||
LOG.info('MAINTENANCE_COMPLETE controller %s' % host)
|
||||
@ -904,11 +906,13 @@ class Workflow(BaseWorkflow):
|
||||
self._wait_host_empty(host)
|
||||
|
||||
LOG.info('IN_MAINTENANCE compute %s' % host)
|
||||
self._admin_notify(self.conf.workflow_project, host,
|
||||
self._admin_notify(self.conf.service_user.os_project_name,
|
||||
host,
|
||||
'IN_MAINTENANCE',
|
||||
self.session_id)
|
||||
self.host_maintenance(host)
|
||||
self._admin_notify(self.conf.workflow_project, host,
|
||||
self._admin_notify(self.conf.service_user.os_project_name,
|
||||
host,
|
||||
'MAINTENANCE_COMPLETE',
|
||||
self.session_id)
|
||||
|
||||
@ -923,11 +927,13 @@ class Workflow(BaseWorkflow):
|
||||
self._wait_host_empty(host)
|
||||
|
||||
LOG.info('IN_MAINTENANCE host %s' % host)
|
||||
self._admin_notify(self.conf.workflow_project, host,
|
||||
self._admin_notify(self.conf.service_user.os_project_name,
|
||||
host,
|
||||
'IN_MAINTENANCE',
|
||||
self.session_id)
|
||||
self.host_maintenance(host)
|
||||
self._admin_notify(self.conf.workflow_project, host,
|
||||
self._admin_notify(self.conf.service_user.os_project_name,
|
||||
host,
|
||||
'MAINTENANCE_COMPLETE',
|
||||
self.session_id)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user