From e1f9a5dfe0d79b2418ef3af4dd9199573cade80c Mon Sep 17 00:00:00 2001 From: adriant Date: Tue, 17 May 2016 16:15:53 +1200 Subject: [PATCH] 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 --- conf/conf.yaml | 24 ++++++++++----- stacktask/api/__init__.py | 1 + stacktask/api/startup.py | 58 +++++++++++++++++++++++++++++++++++ stacktask/api/v1/models.py | 31 ++++++++++++++++++- stacktask/api/v1/openstack.py | 2 +- stacktask/api/v1/tasks.py | 42 +++++++++++++++---------- stacktask/api/v1/urls.py | 26 ++++------------ stacktask/exceptions.py | 30 ++++++++++++++++++ stacktask/settings.py | 18 +++++++++-- stacktask/test_settings.py | 14 +++++++++ 10 files changed, 199 insertions(+), 47 deletions(-) create mode 100644 stacktask/api/startup.py create mode 100644 stacktask/exceptions.py diff --git a/conf/conf.yaml b/conf/conf.yaml index e07fd45..2fc014a 100644 --- a/conf/conf.yaml +++ b/conf/conf.yaml @@ -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: diff --git a/stacktask/api/__init__.py b/stacktask/api/__init__.py index e69de29..e29f5f2 100644 --- a/stacktask/api/__init__.py +++ b/stacktask/api/__init__.py @@ -0,0 +1 @@ +default_app_config = 'stacktask.api.startup.APIConfig' diff --git a/stacktask/api/startup.py b/stacktask/api/startup.py new file mode 100644 index 0000000..27c6703 --- /dev/null +++ b/stacktask/api/startup.py @@ -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() diff --git a/stacktask/api/v1/models.py b/stacktask/api/v1/models.py index aa1603c..86a33da 100644 --- a/stacktask/api/v1/models.py +++ b/stacktask/api/v1/models.py @@ -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\w+)/?$', openstack.UserDetail) +register_taskview_class( + r'^openstack/users/(?P\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) diff --git a/stacktask/api/v1/openstack.py b/stacktask/api/v1/openstack.py index a4dc60c..8fc0966 100644 --- a/stacktask/api/v1/openstack.py +++ b/stacktask/api/v1/openstack.py @@ -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 diff --git a/stacktask/api/v1/tasks.py b/stacktask/api/v1/tasks.py index b9e1a2b..4d9d1ed 100644 --- a/stacktask/api/v1/tasks.py +++ b/stacktask/api/v1/tasks.py @@ -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 = [] diff --git a/stacktask/api/v1/urls.py b/stacktask/api/v1/urls.py index 36668c4..d4fd304 100644 --- a/stacktask/api/v1/urls.py +++ b/stacktask/api/v1/urls.py @@ -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\w+)/?$', views.NotificationDetail.as_view()), url(r'^notifications/?$', views.NotificationList.as_view()), - - url(r'^openstack/users/(?P\w+)/roles/?$', - openstack.UserRoles.as_view()), - url(r'^openstack/users/(?P\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()) + ) diff --git a/stacktask/exceptions.py b/stacktask/exceptions.py new file mode 100644 index 0000000..0d487f7 --- /dev/null +++ b/stacktask/exceptions.py @@ -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.""" diff --git a/stacktask/settings.py b/stacktask/settings.py index b155e37..b07d384 100644 --- a/stacktask/settings.py +++ b/stacktask/settings.py @@ -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 = {} diff --git a/stacktask/test_settings.py b/stacktask/test_settings.py index 86e57ff..b1a9816 100644 --- a/stacktask/test_settings.py +++ b/stacktask/test_settings.py @@ -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,