Setup StackTask for plugins

* All non-admin urls are now set in the config.
* All taskviews are registered in the models.py file of api.v1
**  Based in part on how keystone handles it's own plugins, where
    the url will be defined in the modules, and the conf simply
    enables them. Less configurable, but safer.
* StackTask now does a startup check to confirm all expected
  taskviews and actions have been registered
**  Means we can add more startup sanity checks in future too.
* Taskviews 'default_action' is now 'default_actions'
**  'default_actions' can be overridden in conf
* TaskView settings 'actions' renamed to 'additional_actions'

Change-Id: Ic036407cbaf292830cbe60cbed4a8db0be5e87e3
This commit is contained in:
adriant 2016-05-17 16:15:53 +12:00 committed by adrian-turjak
parent 454aa30d83
commit e1f9a5dfe0
10 changed files with 199 additions and 47 deletions

View File

@ -39,9 +39,6 @@ LOGGING:
EMAIL_SETTINGS:
EMAIL_BACKEND: django.core.mail.backends.console.EmailBackend
# Application settings:
SHOW_ACTION_ENDPOINTS: False
# setting to control if user name and email are allowed
# to have different values.
USERNAME_IS_EMAIL: True
@ -60,6 +57,14 @@ TOKEN_SUBMISSION_URL: http://192.168.122.160:8080/token/
# time for the token to expire in hours
TOKEN_EXPIRE_TIME: 24
ACTIVE_TASKVIEWS:
- UserRoles
- UserDetail
- UserResetPassword
- UserSetPassword
- UserList
- RoleList
DEFAULT_TASK_SETTINGS:
emails:
initial:
@ -110,10 +115,15 @@ DEFAULT_TASK_SETTINGS:
# These are cascading overrides for the default settings:
TASK_SETTINGS:
create_project:
# Additonal actions for views:
# - The order of the actions matters. These will run after the
# default action, in the given order.
actions:
# You can override 'default_actions' if needed for given taskviews
# The order of the actions is order of execution.
#
# default_actions:
# - NewProject
#
# Additonal actions for views
# These will run after the default actions, in the given order.
additional_actions:
- AddAdminToProject
- DefaultProjectResources
notifications:

View File

@ -0,0 +1 @@
default_app_config = 'stacktask.api.startup.APIConfig'

58
stacktask/api/startup.py Normal file
View File

@ -0,0 +1,58 @@
from django.apps import AppConfig
from django.conf import settings
from stacktask.exceptions import ActionNotFound, TaskViewNotFound
def check_expected_taskviews():
expected_taskviews = settings.ACTIVE_TASKVIEWS
missing_taskviews = list(
set(expected_taskviews) - set(settings.TASKVIEW_CLASSES.keys()))
if missing_taskviews:
raise TaskViewNotFound(
message=(
"Expected taskviews are unregistered: %s" % missing_taskviews))
def check_expected_actions():
"""Check that all the expected actions have been registered."""
expected_actions = []
for taskview in settings.ACTIVE_TASKVIEWS:
task_class = settings.TASKVIEW_CLASSES.get(taskview)['class']
try:
expected_actions += settings.TASK_SETTINGS.get(
task_class.task_type, {})['default_actions']
except KeyError:
expected_actions += task_class.default_actions
expected_actions += settings.TASK_SETTINGS.get(
task_class.task_type, {}).get('additional_actions', [])
missing_actions = list(
set(expected_actions) - set(settings.ACTION_CLASSES.keys()))
if missing_actions:
raise ActionNotFound(
"Expected actions are unregistered: %s" % missing_actions)
class APIConfig(AppConfig):
name = 'stacktask.api'
def ready(self):
"""A pre-startup function for the api.
Code run here will occur before the API is up and active but after
all models have been loaded.
Useful for any start up checks.
"""
# First check that all expect taskviews are present
check_expected_taskviews()
# Now check if all the actions those views expecte are present.
check_expected_actions()

View File

@ -12,4 +12,33 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.db import models
from django.conf import settings
from stacktask.api.v1 import tasks
from stacktask.api.v1 import openstack
def register_taskview_class(url, taskview_class):
data = {}
data[taskview_class.__name__] = {
'class': taskview_class,
'url': url}
settings.TASKVIEW_CLASSES.update(data)
register_taskview_class(r'^actions/CreateProject/?$', tasks.CreateProject)
register_taskview_class(r'^actions/InviteUser/?$', tasks.InviteUser)
register_taskview_class(r'^actions/ResetPassword/?$', tasks.ResetPassword)
register_taskview_class(r'^actions/EditUser/?$', tasks.EditUser)
register_taskview_class(
r'^openstack/users/?$', openstack.UserList)
register_taskview_class(
r'^openstack/users/(?P<user_id>\w+)/?$', openstack.UserDetail)
register_taskview_class(
r'^openstack/users/(?P<user_id>\w+)/roles/?$', openstack.UserRoles)
register_taskview_class(
r'^openstack/roles/?$', openstack.RoleList)
register_taskview_class(
r'^openstack/users/password-reset?$', openstack.UserResetPassword)
register_taskview_class(
r'^openstack/users/password-set?$', openstack.UserSetPassword)

View File

@ -163,7 +163,7 @@ class UserDetail(tasks.TaskView):
class UserRoles(tasks.TaskView):
default_action = 'EditUserRoles'
default_actions = ['EditUserRoles', ]
task_type = 'edit_roles'
@utils.mod_or_admin

View File

@ -28,15 +28,19 @@ from django.conf import settings
class TaskView(APIViewWithLogger):
"""
Base class for api calls that start a Task.
Until it is moved to settings, 'default_action' is a
required hardcoded field.
'default_actions' is a required hardcoded field.
The default_action is considered the primary action and
will always run first. Additional actions are defined in
the settings file and will run in the order supplied, but
after the default_action.
The default_actions are considered the primary actions and
will always run first (in the given order). Additional actions
are defined in the settings file and will run in the order supplied,
but after the default_actions.
Default actions can be overridden in the settings file as well if
needed.
"""
default_actions = []
def get(self, request):
"""
The get method will return a json listing the actions this
@ -44,9 +48,11 @@ class TaskView(APIViewWithLogger):
"""
class_conf = settings.TASK_SETTINGS.get(self.task_type, {})
actions = [self.default_action, ]
actions = (
class_conf.get('default_actions', []) or
self.default_actions[:])
actions += class_conf.get('actions', [])
actions += class_conf.get('additional_actions', [])
required_fields = []
@ -70,9 +76,11 @@ class TaskView(APIViewWithLogger):
class_conf = settings.TASK_SETTINGS.get(self.task_type, {})
actions = [self.default_action, ]
actions = (
class_conf.get('default_actions', []) or
self.default_actions[:])
actions += class_conf.get('actions', [])
actions += class_conf.get('additional_actions', [])
action_list = []
@ -311,7 +319,7 @@ class CreateProject(TaskView):
task_type = "create_project"
default_action = "NewProject"
default_actions = ["NewProject", ]
def post(self, request, format=None):
"""
@ -344,7 +352,7 @@ class InviteUser(TaskView):
task_type = "invite_user"
default_action = 'NewUser'
default_actions = ['NewUser', ]
@utils.mod_or_admin
def get(self, request):
@ -387,7 +395,7 @@ class ResetPassword(TaskView):
task_type = "reset_password"
default_action = 'ResetUser'
default_actions = ['ResetUser', ]
def post(self, request, format=None):
"""
@ -432,15 +440,17 @@ class EditUser(TaskView):
task_type = "edit_user"
default_action = 'EditUser'
default_actions = ['EditUserRoles', ]
@utils.mod_or_admin
def get(self, request):
class_conf = settings.TASK_SETTINGS.get(self.task_type, {})
actions = [self.default_action, ]
actions = (
class_conf.get('default_actions', []) or
self.default_actions[:])
actions += class_conf.get('actions', [])
actions += class_conf.get('additional_actions', [])
role_blacklist = class_conf.get('role_blacklist', [])
required_fields = []

View File

@ -14,8 +14,6 @@
from django.conf.urls import url
from stacktask.api.v1 import views
from stacktask.api.v1 import tasks
from stacktask.api.v1 import openstack
from django.conf import settings
@ -28,23 +26,11 @@ urlpatterns = [
url(r'^notifications/(?P<uuid>\w+)/?$',
views.NotificationDetail.as_view()),
url(r'^notifications/?$', views.NotificationList.as_view()),
url(r'^openstack/users/(?P<user_id>\w+)/roles/?$',
openstack.UserRoles.as_view()),
url(r'^openstack/users/(?P<user_id>\w+)/?$',
openstack.UserDetail.as_view()),
url(r'^openstack/users/password-reset?$',
openstack.UserResetPassword.as_view()),
url(r'^openstack/users/password-set?$',
openstack.UserSetPassword.as_view()),
url(r'^openstack/users/?$', openstack.UserList.as_view()),
url(r'^openstack/roles/?$', openstack.RoleList.as_view()),
]
if settings.SHOW_ACTION_ENDPOINTS:
urlpatterns = urlpatterns + [
url(r'^actions/CreateProject/?$', tasks.CreateProject.as_view()),
url(r'^actions/InviteUser/?$', tasks.InviteUser.as_view()),
url(r'^actions/ResetPassword/?$', tasks.ResetPassword.as_view()),
url(r'^actions/EditUser/?$', tasks.EditUser.as_view()),
]
for active_view in settings.ACTIVE_TASKVIEWS:
taskview = settings.TASKVIEW_CLASSES[active_view]
urlpatterns.append(
url(taskview['url'], taskview['class'].as_view())
)

30
stacktask/exceptions.py Normal file
View File

@ -0,0 +1,30 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# 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.
class BaseException(Exception):
"""An error occurred."""
def __init__(self, message=None):
self.message = message
def __str__(self):
return self.message or self.__class__.__doc__
class TaskViewNotFound(BaseException):
"""Attempting to setup TaskView that has not been registered."""
class ActionNotFound(BaseException):
"""Attempting to setup Action that has not been registered."""

View File

@ -157,8 +157,6 @@ TOKEN_SUBMISSION_URL = CONFIG['TOKEN_SUBMISSION_URL']
TOKEN_EXPIRE_TIME = CONFIG['TOKEN_EXPIRE_TIME']
SHOW_ACTION_ENDPOINTS = CONFIG['SHOW_ACTION_ENDPOINTS']
TASK_SETTINGS = setup_task_settings(
CONFIG['DEFAULT_TASK_SETTINGS'],
CONFIG['TASK_SETTINGS'])
@ -167,6 +165,22 @@ ACTION_SETTINGS = CONFIG['ACTION_SETTINGS']
ROLES_MAPPING = CONFIG['ROLES_MAPPING']
# Defaults for backwards compatibility.
ACTIVE_TASKVIEWS = CONFIG.get(
'ACTIVE_TASKVIEWS',
[
'UserRoles',
'UserDetail',
'UserResetPassword',
'UserSetPassword',
'UserList',
'RoleList'
])
# Dict of TaskViews and their url_paths.
# - This is populated by registering taskviews.
TASKVIEW_CLASSES = {}
# Dict of actions and their serializers.
# - This is populated from the various model modules at startup:
ACTION_CLASSES = {}

View File

@ -72,6 +72,19 @@ TOKEN_SUBMISSION_URL = 'http://localhost:8080/token/'
TOKEN_EXPIRE_TIME = 24
ACTIVE_TASKVIEWS = [
'UserRoles',
'UserDetail',
'UserResetPassword',
'UserSetPassword',
'UserList',
'RoleList',
'CreateProject',
'InviteUser',
'ResetPassword',
'EditUser',
]
DEFAULT_TASK_SETTINGS = {
'emails': {
'token': {
@ -182,6 +195,7 @@ conf_dict = {
"USERNAME_IS_EMAIL": USERNAME_IS_EMAIL,
"KEYSTONE": KEYSTONE,
"DEFAULT_REGION": DEFAULT_REGION,
"ACTIVE_TASKVIEWS": ACTIVE_TASKVIEWS,
"DEFAULT_TASK_SETTINGS": DEFAULT_TASK_SETTINGS,
"TASK_SETTINGS": TASK_SETTINGS,
"ACTION_SETTINGS": ACTION_SETTINGS,