From 0eaac89b38aaddddc2fe04fe272755ecae96a605 Mon Sep 17 00:00:00 2001 From: Adrian Turjak Date: Thu, 12 Sep 2019 17:52:20 +1200 Subject: [PATCH] Refactor the plugin layer to use entrypoints Introduce the concept of a feature set, which can be registered to an entrypoint. Rework all existing core elements into a 'core' feature set. Remove the ability to add in random django apps, and drop the ablity for plugins to optionally be able to great new DB models. Change-Id: Idc5c3bf3facc44bb615fa4006d417d6f48a16ddc --- adjutant/actions/models.py | 2 +- adjutant/actions/v1/__init__.py | 1 - adjutant/actions/v1/app.py | 7 - adjutant/actions/v1/base.py | 2 + adjutant/actions/v1/misc.py | 3 + adjutant/actions/v1/models.py | 88 --------- adjutant/actions/v1/projects.py | 11 +- adjutant/actions/v1/resources.py | 9 + adjutant/actions/v1/serializers.py | 2 +- adjutant/actions/v1/users.py | 9 + adjutant/api/urls.py | 11 +- adjutant/api/v1/__init__.py | 1 - adjutant/api/v1/app.py | 10 - adjutant/api/v1/base.py | 2 + adjutant/api/v1/models.py | 66 ------- adjutant/api/v1/openstack.py | 16 ++ adjutant/api/v1/tasks.py | 11 ++ adjutant/api/v1/urls.py | 2 +- adjutant/config/django.py | 7 - .../config/{plugin.py => feature_sets.py} | 2 +- adjutant/core.py | 83 +++++++++ adjutant/feature_set.py | 176 ++++++++++++++++++ .../notifications/{tests => v1}/__init__.py | 0 adjutant/notifications/v1/base.py | 60 ++++++ .../notifications/{models.py => v1/email.py} | 70 +------ adjutant/notifications/v1/tests/__init__.py | 0 .../{ => v1}/tests/test_notifications.py | 3 +- adjutant/plugins.py | 46 ----- adjutant/settings.py | 12 +- adjutant/startup/__init__.py | 2 +- adjutant/startup/checks.py | 33 ++-- adjutant/startup/config.py | 40 ++++ adjutant/startup/loading.py | 21 +++ .../tasks/{v1 => }/templates/completed.txt | 0 .../create_project_and_user_completed.txt | 0 .../create_project_and_user_initial.txt | 0 .../create_project_and_user_token.txt | 0 adjutant/tasks/{v1 => }/templates/initial.txt | 0 .../invite_user_to_project_completed.txt | 0 .../invite_user_to_project_token.txt | 0 .../reset_user_password_completed.txt | 0 .../templates/reset_user_password_token.txt | 0 adjutant/tasks/{v1 => }/templates/token.txt | 0 .../templates/update_quota_completed.txt | 0 .../templates/update_user_email_completed.txt | 0 .../templates/update_user_email_started.txt | 0 .../templates/update_user_email_token.txt | 0 adjutant/tasks/v1/__init__.py | 1 - adjutant/tasks/v1/app.py | 7 - adjutant/tasks/v1/base.py | 7 +- adjutant/tasks/v1/models.py | 47 ----- adjutant/tasks/v1/projects.py | 2 +- doc/source/development.rst | 2 +- doc/source/{plugins.rst => feature-sets.rst} | 165 +++++++++++----- doc/source/features.rst | 7 +- doc/source/guide-lines.rst | 16 +- doc/source/index.rst | 2 +- etc/adjutant.yaml | 13 +- .../notes/feature-sets-f363d132c8c377cf.yaml | 17 ++ setup.cfg | 3 + 60 files changed, 634 insertions(+), 463 deletions(-) delete mode 100644 adjutant/actions/v1/app.py delete mode 100644 adjutant/actions/v1/models.py delete mode 100644 adjutant/api/v1/app.py delete mode 100644 adjutant/api/v1/models.py rename adjutant/config/{plugin.py => feature_sets.py} (92%) create mode 100644 adjutant/core.py create mode 100644 adjutant/feature_set.py rename adjutant/notifications/{tests => v1}/__init__.py (100%) create mode 100644 adjutant/notifications/v1/base.py rename adjutant/notifications/{models.py => v1/email.py} (69%) create mode 100644 adjutant/notifications/v1/tests/__init__.py rename adjutant/notifications/{ => v1}/tests/test_notifications.py (98%) delete mode 100644 adjutant/plugins.py create mode 100644 adjutant/startup/config.py create mode 100644 adjutant/startup/loading.py rename adjutant/tasks/{v1 => }/templates/completed.txt (100%) rename adjutant/tasks/{v1 => }/templates/create_project_and_user_completed.txt (100%) rename adjutant/tasks/{v1 => }/templates/create_project_and_user_initial.txt (100%) rename adjutant/tasks/{v1 => }/templates/create_project_and_user_token.txt (100%) rename adjutant/tasks/{v1 => }/templates/initial.txt (100%) rename adjutant/tasks/{v1 => }/templates/invite_user_to_project_completed.txt (100%) rename adjutant/tasks/{v1 => }/templates/invite_user_to_project_token.txt (100%) rename adjutant/tasks/{v1 => }/templates/reset_user_password_completed.txt (100%) rename adjutant/tasks/{v1 => }/templates/reset_user_password_token.txt (100%) rename adjutant/tasks/{v1 => }/templates/token.txt (100%) rename adjutant/tasks/{v1 => }/templates/update_quota_completed.txt (100%) rename adjutant/tasks/{v1 => }/templates/update_user_email_completed.txt (100%) rename adjutant/tasks/{v1 => }/templates/update_user_email_started.txt (100%) rename adjutant/tasks/{v1 => }/templates/update_user_email_token.txt (100%) delete mode 100644 adjutant/tasks/v1/app.py delete mode 100644 adjutant/tasks/v1/models.py rename doc/source/{plugins.rst => feature-sets.rst} (57%) create mode 100644 releasenotes/notes/feature-sets-f363d132c8c377cf.yaml diff --git a/adjutant/actions/models.py b/adjutant/actions/models.py index d5525a1..c0c3e98 100644 --- a/adjutant/actions/models.py +++ b/adjutant/actions/models.py @@ -46,5 +46,5 @@ class Action(models.Model): def get_action(self): """Returns self as the appropriate action wrapper type.""" data = self.action_data - return actions.ACTION_CLASSES[self.action_name][0]( + return actions.ACTION_CLASSES[self.action_name]( data=data, action_model=self) diff --git a/adjutant/actions/v1/__init__.py b/adjutant/actions/v1/__init__.py index 84334ba..e69de29 100644 --- a/adjutant/actions/v1/__init__.py +++ b/adjutant/actions/v1/__init__.py @@ -1 +0,0 @@ -default_app_config = 'adjutant.actions.v1.app.ActionV1Config' diff --git a/adjutant/actions/v1/app.py b/adjutant/actions/v1/app.py deleted file mode 100644 index ef12ce5..0000000 --- a/adjutant/actions/v1/app.py +++ /dev/null @@ -1,7 +0,0 @@ - -from django.apps import AppConfig - - -class ActionV1Config(AppConfig): - name = "adjutant.actions.v1" - label = 'actions_v1' diff --git a/adjutant/actions/v1/base.py b/adjutant/actions/v1/base.py index 1d1f7fe..a720d56 100644 --- a/adjutant/actions/v1/base.py +++ b/adjutant/actions/v1/base.py @@ -60,6 +60,8 @@ class BaseAction(object): required = [] + serializer = None + config_group = None def __init__(self, data, action_model=None, task=None, diff --git a/adjutant/actions/v1/misc.py b/adjutant/actions/v1/misc.py index 8784895..bbee82b 100644 --- a/adjutant/actions/v1/misc.py +++ b/adjutant/actions/v1/misc.py @@ -19,6 +19,7 @@ from confspirator import fields from confspirator import types from adjutant.actions.v1.base import BaseAction +from adjutant.actions.v1 import serializers from adjutant.actions.utils import send_email from adjutant.common import user_store from adjutant.common import constants @@ -95,6 +96,8 @@ def _build_default_email_group(group_name): class SendAdditionalEmailAction(BaseAction): + serializer = serializers.SendAdditionalEmailSerializer + config_group = groups.DynamicNameConfigGroup( children=[ _build_default_email_group("prepare"), diff --git a/adjutant/actions/v1/models.py b/adjutant/actions/v1/models.py deleted file mode 100644 index ad11b95..0000000 --- a/adjutant/actions/v1/models.py +++ /dev/null @@ -1,88 +0,0 @@ -# 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. - -from rest_framework import serializers as drf_serializers - -from adjutant import actions -from adjutant.actions.v1 import serializers -from adjutant.actions.v1.base import BaseAction -from adjutant.actions.v1.projects import ( - NewProjectWithUserAction, NewProjectAction, - AddDefaultUsersToProjectAction) -from adjutant.actions.v1.users import ( - EditUserRolesAction, NewUserAction, ResetUserPasswordAction, - UpdateUserEmailAction) -from adjutant.actions.v1.resources import ( - NewDefaultNetworkAction, NewProjectDefaultNetworkAction, - SetProjectQuotaAction, UpdateProjectQuotasAction) -from adjutant.actions.v1.misc import SendAdditionalEmailAction -from adjutant import exceptions -from adjutant.config.workflow import action_defaults_group as action_config - - -# Update ACTION_CLASSES dict with tuples in the format: -# (, ) -def register_action_class(action_class, serializer_class): - if not issubclass(action_class, BaseAction): - raise exceptions.InvalidActionClass( - "'%s' is not a built off the BaseAction class." - % action_class.__name__ - ) - if serializer_class and not issubclass( - serializer_class, drf_serializers.Serializer): - raise exceptions.InvalidActionSerializer( - "serializer for '%s' is not a valid DRF serializer." - % action_class.__name__ - ) - data = {} - data[action_class.__name__] = (action_class, serializer_class) - actions.ACTION_CLASSES.update(data) - if action_class.config_group: - # NOTE(adriant): We copy the config_group before naming it - # to avoid cases where a subclass inherits but doesn't extend it - setting_group = action_class.config_group.copy() - setting_group.set_name( - action_class.__name__, reformat_name=False) - action_config.register_child_config(setting_group) - - -# Register Project actions: -register_action_class( - NewProjectWithUserAction, serializers.NewProjectWithUserSerializer) -register_action_class(NewProjectAction, serializers.NewProjectSerializer) -register_action_class( - AddDefaultUsersToProjectAction, - serializers.AddDefaultUsersToProjectSerializer) - -# Register User actions: -register_action_class(NewUserAction, serializers.NewUserSerializer) -register_action_class(ResetUserPasswordAction, serializers.ResetUserSerializer) -register_action_class(EditUserRolesAction, serializers.EditUserRolesSerializer) -register_action_class( - UpdateUserEmailAction, serializers.UpdateUserEmailSerializer) - -# Register Resource actions: -register_action_class( - NewDefaultNetworkAction, serializers.NewDefaultNetworkSerializer) -register_action_class( - NewProjectDefaultNetworkAction, - serializers.NewProjectDefaultNetworkSerializer) -register_action_class( - SetProjectQuotaAction, serializers.SetProjectQuotaSerializer) -register_action_class( - UpdateProjectQuotasAction, serializers.UpdateProjectQuotasSerializer) - -# Register Misc actions: -register_action_class( - SendAdditionalEmailAction, serializers.SendAdditionalEmailSerializer) diff --git a/adjutant/actions/v1/projects.py b/adjutant/actions/v1/projects.py index a43e682..283f141 100644 --- a/adjutant/actions/v1/projects.py +++ b/adjutant/actions/v1/projects.py @@ -14,17 +14,18 @@ from uuid import uuid4 +from django.utils import timezone + from confspirator import groups from confspirator import fields -from django.utils import timezone - from adjutant.config import CONF from adjutant.common import user_store from adjutant.common.utils import str_datetime from adjutant.actions.utils import validate_steps from adjutant.actions.v1.base import ( BaseAction, UserNameAction, UserMixin, ProjectMixin) +from adjutant.actions.v1 import serializers class NewProjectAction(BaseAction, ProjectMixin, UserMixin): @@ -41,6 +42,8 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin): 'description', ] + serializer = serializers.NewProjectSerializer + config_group = groups.DynamicNameConfigGroup( children=[ fields.ListConfig( @@ -149,6 +152,8 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin): 'email' ] + serializer = serializers.NewProjectWithUserSerializer + config_group = groups.DynamicNameConfigGroup( children=[ fields.ListConfig( @@ -439,6 +444,8 @@ class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin): 'domain_id', ] + serializer = serializers.AddDefaultUsersToProjectSerializer + config_group = groups.DynamicNameConfigGroup( children=[ fields.ListConfig( diff --git a/adjutant/actions/v1/resources.py b/adjutant/actions/v1/resources.py index fd763b3..af609e8 100644 --- a/adjutant/actions/v1/resources.py +++ b/adjutant/actions/v1/resources.py @@ -20,6 +20,7 @@ from confspirator import groups from confspirator import fields from adjutant.actions.v1.base import BaseAction, ProjectMixin, QuotaMixin +from adjutant.actions.v1 import serializers from adjutant.actions.utils import validate_steps from adjutant.common import openstack_clients, user_store from adjutant.api import models @@ -40,6 +41,8 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin): 'region', ] + serializer = serializers.NewDefaultNetworkSerializer + config_group = groups.DynamicNameConfigGroup( children=[ groups.ConfigGroup( @@ -232,6 +235,8 @@ class NewProjectDefaultNetworkAction(NewDefaultNetworkAction): 'region', ] + serializer = serializers.NewProjectDefaultNetworkSerializer + def _pre_validate(self): # Note: Don't check project here as it doesn't exist yet. self.action.valid = validate_steps([ @@ -266,6 +271,8 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin): 'regions', ] + serializer = serializers.UpdateProjectQuotasSerializer + config_group = groups.DynamicNameConfigGroup( children=[ fields.FloatConfig( @@ -429,6 +436,8 @@ class SetProjectQuotaAction(UpdateProjectQuotasAction): """ Updates quota for a given project to a configured quota level """ required = [] + serializer = serializers.SetProjectQuotaSerializer + config_group = UpdateProjectQuotasAction.config_group.extend( children=[ fields.DictConfig( diff --git a/adjutant/actions/v1/serializers.py b/adjutant/actions/v1/serializers.py index 4f72048..77e4ea8 100644 --- a/adjutant/actions/v1/serializers.py +++ b/adjutant/actions/v1/serializers.py @@ -80,7 +80,7 @@ class NewProjectWithUserSerializer(BaseUserNameSerializer): project_name = serializers.CharField(max_length=64) -class ResetUserSerializer(BaseUserNameSerializer): +class ResetUserPasswordSerializer(BaseUserNameSerializer): domain_name = serializers.CharField(max_length=64, default='Default') # override domain_id so serializer doesn't set it up. domain_id = None diff --git a/adjutant/actions/v1/users.py b/adjutant/actions/v1/users.py index 7723b4f..f9a25aa 100644 --- a/adjutant/actions/v1/users.py +++ b/adjutant/actions/v1/users.py @@ -19,6 +19,7 @@ from adjutant.config import CONF from adjutant.common import user_store from adjutant.actions.v1.base import ( UserNameAction, UserIdAction, UserMixin, ProjectMixin) +from adjutant.actions.v1 import serializers from adjutant.actions.utils import validate_steps @@ -39,6 +40,8 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin): 'domain_id', ] + serializer = serializers.NewUserSerializer + def _validate_target_user(self): id_manager = user_store.IdentityManager() @@ -181,6 +184,8 @@ class ResetUserPasswordAction(UserNameAction, UserMixin): 'email' ] + serializer = serializers.ResetUserPasswordSerializer + config_group = groups.DynamicNameConfigGroup( children=[ fields.ListConfig( @@ -267,6 +272,8 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin): 'remove' ] + serializer = serializers.EditUserRolesSerializer + def _validate_target_user(self): # Get target user user = self._get_target_user() @@ -403,6 +410,8 @@ class UpdateUserEmailAction(UserIdAction, UserMixin): 'new_email', ] + serializer = serializers.UpdateUserEmailSerializer + def _get_email(self): # Sending to new email address return self.new_email diff --git a/adjutant/api/urls.py b/adjutant/api/urls.py index f769eee..303cadd 100644 --- a/adjutant/api/urls.py +++ b/adjutant/api/urls.py @@ -12,24 +12,23 @@ # License for the specific language governing permissions and limitations # under the License. -from django.apps import apps from django.conf.urls import url, include from django.conf import settings from rest_framework_swagger.views import get_swagger_view from adjutant.api import views +from adjutant.api.views import build_version_details from adjutant.api.v1 import views as views_v1 urlpatterns = [ url(r'^$', views.VersionView.as_view()), ] -# NOTE(adriant): This may not be the best approach, but it does work. Will -# gladly accept a cleaner alternative if it presents itself. -if apps.is_installed('adjutant.api.v1'): - urlpatterns.append(url(r'^v1/?$', views_v1.V1VersionEndpoint.as_view())) - urlpatterns.append(url(r'^v1/', include('adjutant.api.v1.urls'))) +# NOTE(adriant): make this conditional once we have a v2. +build_version_details('1.0', 'CURRENT', relative_endpoint='v1/') +urlpatterns.append(url(r'^v1/?$', views_v1.V1VersionEndpoint.as_view())) +urlpatterns.append(url(r'^v1/', include('adjutant.api.v1.urls'))) if settings.DEBUG: diff --git a/adjutant/api/v1/__init__.py b/adjutant/api/v1/__init__.py index 0f1fc40..e69de29 100644 --- a/adjutant/api/v1/__init__.py +++ b/adjutant/api/v1/__init__.py @@ -1 +0,0 @@ -default_app_config = 'adjutant.api.v1.app.APIV1Config' diff --git a/adjutant/api/v1/app.py b/adjutant/api/v1/app.py deleted file mode 100644 index dd52e5b..0000000 --- a/adjutant/api/v1/app.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.apps import AppConfig -from adjutant.api.views import build_version_details - - -class APIV1Config(AppConfig): - name = "adjutant.api.v1" - label = 'api_v1' - - def ready(self): - build_version_details('1.0', 'CURRENT', relative_endpoint='v1/') diff --git a/adjutant/api/v1/base.py b/adjutant/api/v1/base.py index 07fc32d..9f0ad49 100644 --- a/adjutant/api/v1/base.py +++ b/adjutant/api/v1/base.py @@ -20,6 +20,8 @@ from adjutant.config import CONF class BaseDelegateAPI(APIViewWithLogger): """Base Class for Adjutant's deployer configurable APIs.""" + url = None + config_group = None def __init__(self, *args, **kwargs): diff --git a/adjutant/api/v1/models.py b/adjutant/api/v1/models.py deleted file mode 100644 index 332ac40..0000000 --- a/adjutant/api/v1/models.py +++ /dev/null @@ -1,66 +0,0 @@ -# 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. - -from adjutant import api -from adjutant.api.v1 import tasks -from adjutant.api.v1 import openstack -from adjutant.api.v1.base import BaseDelegateAPI -from adjutant import exceptions -from adjutant.config.api import delegate_apis_group as api_config - - -def register_delegate_api_class(url, api_class): - if not issubclass(api_class, BaseDelegateAPI): - raise exceptions.InvalidAPIClass( - "'%s' is not a built off the BaseDelegateAPI class." - % api_class.__name__ - ) - data = {} - data[api_class.__name__] = { - 'class': api_class, - 'url': url} - api.DELEGATE_API_CLASSES.update(data) - if api_class.config_group: - # NOTE(adriant): We copy the config_group before naming it - # to avoid cases where a subclass inherits but doesn't extend it - setting_group = api_class.config_group.copy() - setting_group.set_name( - api_class.__name__, reformat_name=False) - api_config.register_child_config(setting_group) - - -register_delegate_api_class( - r'^actions/CreateProjectAndUser/?$', tasks.CreateProjectAndUser) -register_delegate_api_class(r'^actions/InviteUser/?$', tasks.InviteUser) -register_delegate_api_class(r'^actions/ResetPassword/?$', tasks.ResetPassword) -register_delegate_api_class(r'^actions/EditUser/?$', tasks.EditUser) -register_delegate_api_class(r'^actions/UpdateEmail/?$', tasks.UpdateEmail) - - -register_delegate_api_class( - r'^openstack/users/?$', openstack.UserList) -register_delegate_api_class( - r'^openstack/users/(?P\w+)/?$', openstack.UserDetail) -register_delegate_api_class( - r'^openstack/users/(?P\w+)/roles/?$', openstack.UserRoles) -register_delegate_api_class( - r'^openstack/roles/?$', openstack.RoleList) -register_delegate_api_class( - r'^openstack/users/password-reset/?$', openstack.UserResetPassword) -register_delegate_api_class( - r'^openstack/users/email-update/?$', openstack.UserUpdateEmail) -register_delegate_api_class( - r'^openstack/sign-up/?$', openstack.SignUp) -register_delegate_api_class( - r'^openstack/quotas/?$', openstack.UpdateProjectQuotas) diff --git a/adjutant/api/v1/openstack.py b/adjutant/api/v1/openstack.py index 8df2021..f30c760 100644 --- a/adjutant/api/v1/openstack.py +++ b/adjutant/api/v1/openstack.py @@ -30,6 +30,8 @@ from adjutant.config import CONF class UserList(tasks.InviteUser): + url = r'^openstack/users/?$' + config_group = groups.DynamicNameConfigGroup( children=[ fields.ListConfig( @@ -168,6 +170,8 @@ class UserList(tasks.InviteUser): class UserDetail(BaseDelegateAPI): + url = r'^openstack/users/(?P\w+)/?$' + config_group = groups.DynamicNameConfigGroup( children=[ fields.ListConfig( @@ -244,6 +248,8 @@ class UserDetail(BaseDelegateAPI): class UserRoles(BaseDelegateAPI): + url = r'^openstack/users/(?P\w+)/roles/?$' + config_group = groups.DynamicNameConfigGroup( children=[ fields.ListConfig( @@ -317,6 +323,8 @@ class UserRoles(BaseDelegateAPI): class RoleList(BaseDelegateAPI): + url = r'^openstack/roles/?$' + @utils.mod_or_admin def get(self, request): """Returns a list of roles that may be managed for this project""" @@ -343,6 +351,8 @@ class UserResetPassword(tasks.ResetPassword): --- """ + url = r'^openstack/users/password-reset/?$' + pass @@ -352,6 +362,8 @@ class UserUpdateEmail(tasks.UpdateEmail): --- """ + url = r'^openstack/users/email-update/?$' + pass @@ -360,6 +372,8 @@ class SignUp(tasks.CreateProjectAndUser): The openstack endpoint for signups. """ + url = r'^openstack/sign-up/?$' + pass @@ -369,6 +383,8 @@ class UpdateProjectQuotas(BaseDelegateAPI): one or more regions """ + url = r'^openstack/quotas/?$' + task_type = "update_quota" _number_of_returned_tasks = 5 diff --git a/adjutant/api/v1/tasks.py b/adjutant/api/v1/tasks.py index dc423c3..b069673 100644 --- a/adjutant/api/v1/tasks.py +++ b/adjutant/api/v1/tasks.py @@ -29,6 +29,8 @@ from adjutant.api.v1.base import BaseDelegateAPI class CreateProjectAndUser(BaseDelegateAPI): + url = r'^actions/CreateProjectAndUser/?$' + config_group = groups.DynamicNameConfigGroup( children=[ fields.StrConfig( @@ -83,6 +85,8 @@ class CreateProjectAndUser(BaseDelegateAPI): class InviteUser(BaseDelegateAPI): + url = r'^actions/InviteUser/?$' + task_type = "invite_user_to_project" @utils.mod_or_admin @@ -118,6 +122,8 @@ class InviteUser(BaseDelegateAPI): class ResetPassword(BaseDelegateAPI): + url = r'^actions/ResetPassword/?$' + task_type = "reset_user_password" @utils.minimal_duration(min_time=3) @@ -160,6 +166,8 @@ class ResetPassword(BaseDelegateAPI): class EditUser(BaseDelegateAPI): + url = r'^actions/EditUser/?$' + task_type = "edit_user_roles" @utils.mod_or_admin @@ -179,6 +187,9 @@ class EditUser(BaseDelegateAPI): class UpdateEmail(BaseDelegateAPI): + + url = r'^actions/UpdateEmail/?$' + task_type = "update_user_email" @utils.authenticated diff --git a/adjutant/api/v1/urls.py b/adjutant/api/v1/urls.py index 57a33bb..eeaa585 100644 --- a/adjutant/api/v1/urls.py +++ b/adjutant/api/v1/urls.py @@ -33,5 +33,5 @@ for active_view in CONF.api.active_delegate_apis: delegate_api = api.DELEGATE_API_CLASSES[active_view] urlpatterns.append( - url(delegate_api['url'], delegate_api['class'].as_view()) + url(delegate_api.url, delegate_api.as_view()) ) diff --git a/adjutant/config/django.py b/adjutant/config/django.py index 4958ea1..a6c766e 100644 --- a/adjutant/config/django.py +++ b/adjutant/config/django.py @@ -45,13 +45,6 @@ config_group.register_child_config( unsafe_default=True, ) ) -config_group.register_child_config( - fields.ListConfig( - "additional_apps", - help_text="A list of additional django apps.", - default=[] - ) -) config_group.register_child_config( fields.DictConfig( "databases", diff --git a/adjutant/config/plugin.py b/adjutant/config/feature_sets.py similarity index 92% rename from adjutant/config/plugin.py rename to adjutant/config/feature_sets.py index 69ec036..b1835fe 100644 --- a/adjutant/config/plugin.py +++ b/adjutant/config/feature_sets.py @@ -15,4 +15,4 @@ from confspirator import groups -config_group = groups.ConfigGroup("plugin") +config_group = groups.ConfigGroup("feature_sets") diff --git a/adjutant/core.py b/adjutant/core.py new file mode 100644 index 0000000..9d3c77b --- /dev/null +++ b/adjutant/core.py @@ -0,0 +1,83 @@ +# Copyright (C) 2019 Catalyst Cloud 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. + +from adjutant.feature_set import BaseFeatureSet + +from adjutant.actions.v1 import misc as misc_actions +from adjutant.actions.v1 import projects as project_actions +from adjutant.actions.v1 import resources as resource_actions +from adjutant.actions.v1 import users as user_actions + +from adjutant.api.v1 import openstack as openstack_apis +from adjutant.api.v1 import tasks as task_apis + +from adjutant.tasks.v1 import projects as project_tasks +from adjutant.tasks.v1 import resources as resource_tasks +from adjutant.tasks.v1 import users as user_tasks + +from adjutant.notifications.v1 import email as email_handlers + + +class AdjutantCore(BaseFeatureSet): + """Adjutant's Core feature set.""" + + actions = [ + project_actions.NewProjectWithUserAction, + project_actions.NewProjectAction, + project_actions.AddDefaultUsersToProjectAction, + + resource_actions.NewDefaultNetworkAction, + resource_actions.NewProjectDefaultNetworkAction, + resource_actions.SetProjectQuotaAction, + resource_actions.UpdateProjectQuotasAction, + + user_actions.NewUserAction, + user_actions.ResetUserPasswordAction, + user_actions.EditUserRolesAction, + user_actions.UpdateUserEmailAction, + + misc_actions.SendAdditionalEmailAction, + ] + + tasks = [ + project_tasks.CreateProjectAndUser, + + user_tasks.EditUserRoles, + user_tasks.InviteUser, + user_tasks.ResetUserPassword, + user_tasks.UpdateUserEmail, + + resource_tasks.UpdateProjectQuotas, + ] + + delegate_apis = [ + task_apis.CreateProjectAndUser, + task_apis.InviteUser, + task_apis.ResetPassword, + task_apis.EditUser, + task_apis.UpdateEmail, + + openstack_apis.UserList, + openstack_apis.UserDetail, + openstack_apis.UserRoles, + openstack_apis.RoleList, + openstack_apis.UserResetPassword, + openstack_apis.UserUpdateEmail, + openstack_apis.SignUp, + openstack_apis.UpdateProjectQuotas, + ] + + notification_handlers = [ + email_handlers.EmailNotification, + ] diff --git a/adjutant/feature_set.py b/adjutant/feature_set.py new file mode 100644 index 0000000..6185d4b --- /dev/null +++ b/adjutant/feature_set.py @@ -0,0 +1,176 @@ +# Copyright (C) 2019 Catalyst Cloud 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. + +from logging import getLogger + +from rest_framework import serializers as drf_serializers + +from confspirator import exceptions as conf_exceptions +from confspirator import groups + +from adjutant import exceptions + +from adjutant import actions +from adjutant.actions.v1.base import BaseAction +from adjutant.config.workflow import action_defaults_group + +from adjutant import tasks +from adjutant.tasks.v1 import base as tasks_base +from adjutant.config.workflow import tasks_group + +from adjutant import api +from adjutant.api.v1.base import BaseDelegateAPI +from adjutant.config.api import delegate_apis_group as api_config + +from adjutant import notifications +from adjutant.notifications.v1.base import BaseNotificationHandler +from adjutant.config.notification import handler_defaults_group + +from adjutant.config.feature_sets import config_group as feature_set_config + + +def register_action_class(action_class): + if not issubclass(action_class, BaseAction): + raise exceptions.InvalidActionClass( + "'%s' is not a built off the BaseAction class." + % action_class.__name__ + ) + if action_class.serializer and not issubclass( + action_class.serializer, drf_serializers.Serializer): + raise exceptions.InvalidActionSerializer( + "serializer for '%s' is not a valid DRF serializer." + % action_class.__name__ + ) + data = {} + data[action_class.__name__] = action_class + actions.ACTION_CLASSES.update(data) + if action_class.config_group: + # NOTE(adriant): We copy the config_group before naming it + # to avoid cases where a subclass inherits but doesn't extend it + setting_group = action_class.config_group.copy() + setting_group.set_name( + action_class.__name__, reformat_name=False) + action_defaults_group.register_child_config(setting_group) + + +def register_task_class(task_class): + if not issubclass(task_class, tasks_base.BaseTask): + raise exceptions.InvalidTaskClass( + "'%s' is not a built off the BaseTask class." + % task_class.__name__ + ) + data = {} + data[task_class.task_type] = task_class + if task_class.deprecated_task_types: + for old_type in task_class.deprecated_task_types: + data[old_type] = task_class + tasks.TASK_CLASSES.update(data) + + config_group = tasks_base.make_task_config(task_class) + config_group.set_name( + task_class.task_type, reformat_name=False) + tasks_group.register_child_config(config_group) + + +def register_delegate_api_class(api_class): + if not issubclass(api_class, BaseDelegateAPI): + raise exceptions.InvalidAPIClass( + "'%s' is not a built off the BaseDelegateAPI class." + % api_class.__name__ + ) + data = {} + data[api_class.__name__] = api_class + api.DELEGATE_API_CLASSES.update(data) + if api_class.config_group: + # NOTE(adriant): We copy the config_group before naming it + # to avoid cases where a subclass inherits but doesn't extend it + setting_group = api_class.config_group.copy() + setting_group.set_name( + api_class.__name__, reformat_name=False) + api_config.register_child_config(setting_group) + + +def register_notification_handler(notification_handler): + if not issubclass(notification_handler, BaseNotificationHandler): + raise exceptions.InvalidActionClass( + "'%s' is not a built off the BaseNotificationHandler class." + % notification_handler.__name__ + ) + notifications.NOTIFICATION_HANDLERS[ + notification_handler.__name__ + ] = notification_handler + if notification_handler.config_group: + # NOTE(adriant): We copy the config_group before naming it + # to avoid cases where a subclass inherits but doesn't extend it + setting_group = notification_handler.config_group.copy() + setting_group.set_name(notification_handler.__name__, reformat_name=False) + handler_defaults_group.register_child_config(setting_group) + + +def register_feature_set_config(feature_set_group): + if not isinstance(feature_set_group, groups.ConfigGroup): + raise conf_exceptions.InvalidConfigClass( + "'%s' is not a valid config group class" % feature_set_group) + feature_set_config.register_child_config(feature_set_group) + + +class BaseFeatureSet(object): + """A grouping of Adjutant pluggable features. + + Contains within it definitions for: + - actions + - tasks + - delegate_apis + - notification_handlers + + And additional feature set specific config: + - config + + These are just lists of the appropriate class types, and will + imported into Adjutant when the featureset is included. + """ + + actions = None + tasks = None + delegate_apis = None + notification_handlers = None + + config = None + + def __init__(self): + self.logger = getLogger('adjutant') + + def load(self): + self.logger.info("Loading feature set: '%s'" % self.__class__.__name__) + + if self.actions: + for action in self.actions: + register_action_class(action) + + if self.tasks: + for task in self.tasks: + register_task_class(task) + + if self.delegate_apis: + for delegate_api in self.delegate_apis: + register_delegate_api_class(delegate_api) + + if self.notification_handlers: + for notification_handler in self.notification_handlers: + register_notification_handler(notification_handler) + + if self.config: + if isinstance(self.config, groups.DynamicNameConfigGroup): + self.config.set_name(self.__class__.__name__) + register_feature_set_config(self.config) diff --git a/adjutant/notifications/tests/__init__.py b/adjutant/notifications/v1/__init__.py similarity index 100% rename from adjutant/notifications/tests/__init__.py rename to adjutant/notifications/v1/__init__.py diff --git a/adjutant/notifications/v1/base.py b/adjutant/notifications/v1/base.py new file mode 100644 index 0000000..f3f06e6 --- /dev/null +++ b/adjutant/notifications/v1/base.py @@ -0,0 +1,60 @@ +# 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. + +from logging import getLogger + +from adjutant.config import CONF + + +class BaseNotificationHandler(object): + """""" + + config_group = None + + def __init__(self): + self.logger = getLogger("adjutant") + + def config(self, task, notification): + """build config based on conf and defaults + + Will use the Handler defaults, and the overlay them with more + specific overrides from the task defaults, and the per task + type config. + """ + try: + notif_config = CONF.notifications.handler_defaults.get( + self.__class__.__name__) + except KeyError: + # Handler has no config + return {} + + task_defaults = task.config.notifications + + try: + if notification.error: + task_defaults = task_defaults.error_handler_config.get( + self.__class__.__name__) + else: + task_defaults = task_defaults.standard_handler_config.get( + self.__class__.__name__) + except KeyError: + task_defaults = {} + + return notif_config.overlay(task_defaults) + + def notify(self, task, notification): + return self._notify(task, notification) + + def _notify(self, task, notification): + raise NotImplementedError diff --git a/adjutant/notifications/models.py b/adjutant/notifications/v1/email.py similarity index 69% rename from adjutant/notifications/models.py rename to adjutant/notifications/v1/email.py index 9dff617..82ca6d0 100644 --- a/adjutant/notifications/models.py +++ b/adjutant/notifications/v1/email.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -from logging import getLogger from smtplib import SMTPException from django.core.mail import EmailMultiAlternatives @@ -25,56 +24,11 @@ from confspirator import types from adjutant.config import CONF from adjutant.common import constants -from adjutant import notifications from adjutant.api.models import Notification -from adjutant import exceptions -from adjutant.config.notification import handler_defaults_group +from adjutant.notifications.v1 import base -class BaseNotificationHandler(object): - """""" - - config_group = None - - def __init__(self): - self.logger = getLogger("adjutant") - - def config(self, task, notification): - """build config based on conf and defaults - - Will use the Handler defaults, and the overlay them with more - specific overrides from the task defaults, and the per task - type config. - """ - try: - notif_config = CONF.notifications.handler_defaults.get( - self.__class__.__name__) - except KeyError: - # Handler has no config - return {} - - task_defaults = task.config.notifications - - try: - if notification.error: - task_defaults = task_defaults.error_handler_config.get( - self.__class__.__name__) - else: - task_defaults = task_defaults.standard_handler_config.get( - self.__class__.__name__) - except KeyError: - task_defaults = {} - - return notif_config.overlay(task_defaults) - - def notify(self, task, notification): - return self._notify(task, notification) - - def _notify(self, task, notification): - raise NotImplementedError - - -class EmailNotification(BaseNotificationHandler): +class EmailNotification(base.BaseNotificationHandler): """ Basic email notification handler. Will send an email with the given templates. @@ -186,23 +140,3 @@ class EmailNotification(BaseNotificationHandler): task=notification.task, notes=notes, error=True ) error_notification.save() - - -def register_notification_handler(notification_handler): - if not issubclass(notification_handler, BaseNotificationHandler): - raise exceptions.InvalidActionClass( - "'%s' is not a built off the BaseNotificationHandler class." - % notification_handler.__name__ - ) - notifications.NOTIFICATION_HANDLERS[ - notification_handler.__name__ - ] = notification_handler - if notification_handler.config_group: - # NOTE(adriant): We copy the config_group before naming it - # to avoid cases where a subclass inherits but doesn't extend it - setting_group = notification_handler.config_group.copy() - setting_group.set_name(notification_handler.__name__, reformat_name=False) - handler_defaults_group.register_child_config(setting_group) - - -register_notification_handler(EmailNotification) diff --git a/adjutant/notifications/v1/tests/__init__.py b/adjutant/notifications/v1/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/adjutant/notifications/tests/test_notifications.py b/adjutant/notifications/v1/tests/test_notifications.py similarity index 98% rename from adjutant/notifications/tests/test_notifications.py rename to adjutant/notifications/v1/tests/test_notifications.py index 889e946..9aeeaff 100644 --- a/adjutant/notifications/tests/test_notifications.py +++ b/adjutant/notifications/v1/tests/test_notifications.py @@ -20,7 +20,8 @@ from rest_framework import status from confspirator.tests import utils as conf_utils -from adjutant.api.models import Task, Notification +from adjutant.api.models import Notification +from adjutant.tasks.models import Task from adjutant.common.tests.fake_clients import ( FakeManager, setup_identity_cache) from adjutant.common.tests.utils import AdjutantAPITestCase diff --git a/adjutant/plugins.py b/adjutant/plugins.py deleted file mode 100644 index f7b7fdc..0000000 --- a/adjutant/plugins.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (C) 2019 Catalyst Cloud 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. - -from confspirator import exceptions -from confspirator import groups - -from adjutant.actions.v1 import models as _action_models -from adjutant.api.v1 import models as _api_models -from adjutant.notifications import models as _notif_models -from adjutant.tasks.v1 import models as _task_models - -from adjutant.config.plugin import config_group as _config_group - - -def register_plugin_config(plugin_group): - if not isinstance(plugin_group, groups.ConfigGroup): - raise exceptions.InvalidConfigClass( - "'%s' is not a valid config group class" % plugin_group) - _config_group.register_child_config(plugin_group) - - -def register_plugin_action(action_class, serializer_class): - _action_models.register_action_class(action_class, serializer_class) - - -def register_plugin_task(task_class): - _task_models.register_task_class(task_class) - - -def register_plugin_delegate_api(url, api_class): - _api_models.register_delegate_api_class(url, api_class) - - -def register_notification_handler(notification_handler): - _notif_models.register_notification_handler(notification_handler) diff --git a/adjutant/settings.py b/adjutant/settings.py index a0b26ad..d18d87f 100644 --- a/adjutant/settings.py +++ b/adjutant/settings.py @@ -47,11 +47,7 @@ INSTALLED_APPS = ( 'adjutant.api', 'adjutant.notifications', 'adjutant.tasks', - - # NOTE(adriant): Until we have v2 options, hardcode our v1s - 'adjutant.actions.v1', - 'adjutant.tasks.v1', - 'adjutant.api.v1', + 'adjutant.startup', ) MIDDLEWARE_CLASSES = ( @@ -123,12 +119,6 @@ if DEBUG: ALLOWED_HOSTS = adj_conf.django.allowed_hosts -_INSTALLED_APPS = list(INSTALLED_APPS) + adj_conf.django.additional_apps -# NOTE(adriant): Because the order matters, we want this import to be last -# so the startup checks run after everything is imported. -_INSTALLED_APPS.append("adjutant.startup") -INSTALLED_APPS = _INSTALLED_APPS - DATABASES = adj_conf.django.databases if adj_conf.django.logging: diff --git a/adjutant/startup/__init__.py b/adjutant/startup/__init__.py index 60f9f30..4fc1981 100644 --- a/adjutant/startup/__init__.py +++ b/adjutant/startup/__init__.py @@ -1 +1 @@ -default_app_config = 'adjutant.startup.checks.StartUpConfig' +default_app_config = 'adjutant.startup.config.StartUpConfig' diff --git a/adjutant/startup/checks.py b/adjutant/startup/checks.py index f1d87dd..0f0b5b2 100644 --- a/adjutant/startup/checks.py +++ b/adjutant/startup/checks.py @@ -1,4 +1,16 @@ -from django.apps import AppConfig +# Copyright (C) 2019 Catalyst Cloud 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. from adjutant.config import CONF from adjutant import actions, api, tasks @@ -34,22 +46,3 @@ def check_configured_actions(): if missing_actions: raise ActionNotRegistered( "Configured actions are unregistered: %s" % missing_actions) - - -class StartUpConfig(AppConfig): - name = "adjutant.startup" - - 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 DelegateAPIs are present - check_expected_delegate_apis() - - # Now check if all the actions those views expecte are present. - check_configured_actions() diff --git a/adjutant/startup/config.py b/adjutant/startup/config.py new file mode 100644 index 0000000..d0fb274 --- /dev/null +++ b/adjutant/startup/config.py @@ -0,0 +1,40 @@ +# Copyright (C) 2019 Catalyst Cloud 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. + +from django.apps import AppConfig + +from adjutant.startup import checks +from adjutant.startup import loading + + +class StartUpConfig(AppConfig): + name = "adjutant.startup" + + 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. + + Loads feature_sets. + + Useful for any start up checks. + """ + # load all the feature sets + loading.load_feature_sets() + + # First check that all expect DelegateAPIs are present + checks.check_expected_delegate_apis() + # Now check if all the actions those views expecte are present. + checks.check_configured_actions() diff --git a/adjutant/startup/loading.py b/adjutant/startup/loading.py new file mode 100644 index 0000000..096272d --- /dev/null +++ b/adjutant/startup/loading.py @@ -0,0 +1,21 @@ +# Copyright (C) 2019 Catalyst Cloud 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. + +import pkg_resources + + +def load_feature_sets(): + for entry_point in pkg_resources.iter_entry_points('adjutant.feature_sets'): + feature_set = entry_point.load() + feature_set().load() diff --git a/adjutant/tasks/v1/templates/completed.txt b/adjutant/tasks/templates/completed.txt similarity index 100% rename from adjutant/tasks/v1/templates/completed.txt rename to adjutant/tasks/templates/completed.txt diff --git a/adjutant/tasks/v1/templates/create_project_and_user_completed.txt b/adjutant/tasks/templates/create_project_and_user_completed.txt similarity index 100% rename from adjutant/tasks/v1/templates/create_project_and_user_completed.txt rename to adjutant/tasks/templates/create_project_and_user_completed.txt diff --git a/adjutant/tasks/v1/templates/create_project_and_user_initial.txt b/adjutant/tasks/templates/create_project_and_user_initial.txt similarity index 100% rename from adjutant/tasks/v1/templates/create_project_and_user_initial.txt rename to adjutant/tasks/templates/create_project_and_user_initial.txt diff --git a/adjutant/tasks/v1/templates/create_project_and_user_token.txt b/adjutant/tasks/templates/create_project_and_user_token.txt similarity index 100% rename from adjutant/tasks/v1/templates/create_project_and_user_token.txt rename to adjutant/tasks/templates/create_project_and_user_token.txt diff --git a/adjutant/tasks/v1/templates/initial.txt b/adjutant/tasks/templates/initial.txt similarity index 100% rename from adjutant/tasks/v1/templates/initial.txt rename to adjutant/tasks/templates/initial.txt diff --git a/adjutant/tasks/v1/templates/invite_user_to_project_completed.txt b/adjutant/tasks/templates/invite_user_to_project_completed.txt similarity index 100% rename from adjutant/tasks/v1/templates/invite_user_to_project_completed.txt rename to adjutant/tasks/templates/invite_user_to_project_completed.txt diff --git a/adjutant/tasks/v1/templates/invite_user_to_project_token.txt b/adjutant/tasks/templates/invite_user_to_project_token.txt similarity index 100% rename from adjutant/tasks/v1/templates/invite_user_to_project_token.txt rename to adjutant/tasks/templates/invite_user_to_project_token.txt diff --git a/adjutant/tasks/v1/templates/reset_user_password_completed.txt b/adjutant/tasks/templates/reset_user_password_completed.txt similarity index 100% rename from adjutant/tasks/v1/templates/reset_user_password_completed.txt rename to adjutant/tasks/templates/reset_user_password_completed.txt diff --git a/adjutant/tasks/v1/templates/reset_user_password_token.txt b/adjutant/tasks/templates/reset_user_password_token.txt similarity index 100% rename from adjutant/tasks/v1/templates/reset_user_password_token.txt rename to adjutant/tasks/templates/reset_user_password_token.txt diff --git a/adjutant/tasks/v1/templates/token.txt b/adjutant/tasks/templates/token.txt similarity index 100% rename from adjutant/tasks/v1/templates/token.txt rename to adjutant/tasks/templates/token.txt diff --git a/adjutant/tasks/v1/templates/update_quota_completed.txt b/adjutant/tasks/templates/update_quota_completed.txt similarity index 100% rename from adjutant/tasks/v1/templates/update_quota_completed.txt rename to adjutant/tasks/templates/update_quota_completed.txt diff --git a/adjutant/tasks/v1/templates/update_user_email_completed.txt b/adjutant/tasks/templates/update_user_email_completed.txt similarity index 100% rename from adjutant/tasks/v1/templates/update_user_email_completed.txt rename to adjutant/tasks/templates/update_user_email_completed.txt diff --git a/adjutant/tasks/v1/templates/update_user_email_started.txt b/adjutant/tasks/templates/update_user_email_started.txt similarity index 100% rename from adjutant/tasks/v1/templates/update_user_email_started.txt rename to adjutant/tasks/templates/update_user_email_started.txt diff --git a/adjutant/tasks/v1/templates/update_user_email_token.txt b/adjutant/tasks/templates/update_user_email_token.txt similarity index 100% rename from adjutant/tasks/v1/templates/update_user_email_token.txt rename to adjutant/tasks/templates/update_user_email_token.txt diff --git a/adjutant/tasks/v1/__init__.py b/adjutant/tasks/v1/__init__.py index d22e1f6..e69de29 100644 --- a/adjutant/tasks/v1/__init__.py +++ b/adjutant/tasks/v1/__init__.py @@ -1 +0,0 @@ -default_app_config = 'adjutant.tasks.v1.app.TasksV1Config' diff --git a/adjutant/tasks/v1/app.py b/adjutant/tasks/v1/app.py deleted file mode 100644 index 257fbb7..0000000 --- a/adjutant/tasks/v1/app.py +++ /dev/null @@ -1,7 +0,0 @@ - -from django.apps import AppConfig - - -class TasksV1Config(AppConfig): - name = "adjutant.tasks.v1" - label = 'tasks_v1' diff --git a/adjutant/tasks/v1/base.py b/adjutant/tasks/v1/base.py index 22b9545..f6a1331 100644 --- a/adjutant/tasks/v1/base.py +++ b/adjutant/tasks/v1/base.py @@ -201,17 +201,16 @@ class BaseTask(object): else: action_name = action - action_class, serializer_class = \ - adj_actions.ACTION_CLASSES[action_name] + action_class = adj_actions.ACTION_CLASSES[action_name] if use_existing_actions: action_class = action # instantiate serializer class - if not serializer_class: + if not action_class.serializer: raise exceptions.SerializerMissingException( "No serializer defined for action %s" % action_name) - serializer = serializer_class(data=action_data) + serializer = action_class.serializer(data=action_data) action_serializer_list.append({ 'name': action_name, diff --git a/adjutant/tasks/v1/models.py b/adjutant/tasks/v1/models.py deleted file mode 100644 index 65247bb..0000000 --- a/adjutant/tasks/v1/models.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (C) 2019 Catalyst Cloud 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. - -from adjutant import exceptions -from adjutant import tasks -from adjutant.config.workflow import tasks_group as tasks_group -from adjutant.tasks.v1 import base -from adjutant.tasks.v1 import projects, users, resources - - -def register_task_class(task_class): - if not issubclass(task_class, base.BaseTask): - raise exceptions.InvalidTaskClass( - "'%s' is not a built off the BaseTask class." - % task_class.__name__ - ) - data = {} - data[task_class.task_type] = task_class - if task_class.deprecated_task_types: - for old_type in task_class.deprecated_task_types: - data[old_type] = task_class - tasks.TASK_CLASSES.update(data) - setting_group = base.make_task_config(task_class) - setting_group.set_name( - task_class.task_type, reformat_name=False) - tasks_group.register_child_config(setting_group) - - -register_task_class(projects.CreateProjectAndUser) - -register_task_class(users.EditUserRoles) -register_task_class(users.InviteUser) -register_task_class(users.ResetUserPassword) -register_task_class(users.UpdateUserEmail) - -register_task_class(resources.UpdateProjectQuotas) diff --git a/adjutant/tasks/v1/projects.py b/adjutant/tasks/v1/projects.py index 4d9f43f..5d560eb 100644 --- a/adjutant/tasks/v1/projects.py +++ b/adjutant/tasks/v1/projects.py @@ -18,7 +18,7 @@ from adjutant.tasks.v1.base import BaseTask class CreateProjectAndUser(BaseTask): duplicate_policy = "block" task_type = "create_project_and_user" - deprecated_task_types = ['create_project'] + deprecated_task_types = ['create_project', 'signup'] default_actions = [ "NewProjectWithUserAction", ] diff --git a/doc/source/development.rst b/doc/source/development.rst index 751170e..308c0b9 100644 --- a/doc/source/development.rst +++ b/doc/source/development.rst @@ -72,7 +72,7 @@ Actions themselves can also effectively do anything within the scope of those three stages, and there is even the ability to chain multiple actions together, and pass data along to other actions. -Details for adding task and actions can be found on the :doc:`plugins` +Details for adding task and actions can be found on the :doc:`feature-sets` page. diff --git a/doc/source/plugins.rst b/doc/source/feature-sets.rst similarity index 57% rename from doc/source/plugins.rst rename to doc/source/feature-sets.rst index 3eeeb18..7664e9d 100644 --- a/doc/source/plugins.rst +++ b/doc/source/feature-sets.rst @@ -1,30 +1,84 @@ -############################## -Creating Plugins for Adjutant -############################## +################################## +Creating Feature Sets for Adjutant +################################## -As Adjutant is built on top of Django, we've used parts of Django's installed -apps system to allow us a plugin mechanism that allows additional actions and -views to be brought in via external sources. This allows company specific or -deployer specific changes to easily live outside of the core service and simply -extend the core service where and when need. +Adjutant supports the introduction of new Actions, Tasks, and DelegateAPIs +via additional feature sets. A feature set is a bundle of these elements +with maybe some feature set specific extra config. This allows company specific +or deployer specific changes to easily live outside of the core service and +simply extend the core service where and when need. -An example of such a plugin is here: -https://github.com/catalyst/adjutant-odoo +An example of such a plugin is here (although it may not yet be using the new +'feature set' plugin mechanism): +https://github.com/catalyst-cloud/adjutant-odoo + + +Once you have all the Actions, Tasks, DelegateAPIs, or Notification Handlers +that you want to include in a feature set, you register them by making a +feature set class:: + + from adjutant.feature_set import BaseFeatureSet + + from myplugin.actions import MyCustonAction + from myplugin.tasks import MyCustonTask + from myplugin.apis import MyCustonAPI + from myplugin.handlers import MyCustonNotificationHandler + + class MyFeatureSet(BaseFeatureSet): + + actions = [ + MyCustonAction, + ] + + tasks = [ + MyCustonTask, + ] + + delegate_apis = [ + MyCustonAPI, + ] + + notification_handlers = [ + MyCustonNotificationHandler, + ] + + +Then adding it to the library entrypoints:: + + adjutant.feature_sets = + custom_thing = myplugin.features:MyFeatureSet + + +If you need custom config for your plugin that should be accessible +and the same across all your Actions, Tasks, APIs, or Notification Handlers +then you can register config to the feature set itself:: + + from confspirator import groups + + .... + + class MyFeatureSet(BaseFeatureSet): + + ..... + + config = groups.DynamicNameConfigGroup( + children=[ + fields.StrConfig( + 'myconfig', + help_text="Some custom config.", + required=True, + default="Stuff", + ), + ] + ) + +Which will be accessible via Adjutant's config at: +``CONF.feature_sets.MyFeatureSet.myconfig`` Building DelegateAPIs ===================== -New DelegateAPIs should inherit from adjutant.api.v1.base.BaseDelegateAPI -can be registered as such:: - - from adjutant.plugins import register_plugin_delegate_api, - - from myplugin import apis - - register_plugin_delegate_api(r'^my-plugin/some-action/?$', apis.MyAPIView) - -A DelegateAPI must both be registered with a valid URL and specified in -ACTIVE_DELEGATE_APIS in the configuration to be accessible. +New DelegateAPIs should inherit from ``adjutant.api.v1.base.BaseDelegateAPI`` A new DelegateAPI from a plugin can effectively 'override' a default DelegateAPI by registering with the same URL. However it must have @@ -35,7 +89,9 @@ Examples of DelegateAPIs can be found in adjutant.api.v1.openstack Minimally they can look like this:: - class NewCreateProject(BaseDelegateAPI): + class MyCustomAPI(BaseDelegateAPI): + + url = r'^custom/mycoolstuff/?$' @utils.authenticated def post(self, request): @@ -48,18 +104,30 @@ admin decorators found in adjutant.api.utils. The request handlers are fairly standard django view handlers and can execute any needed code. Additional information for the task should be placed in request.data. +You can also add customer config for the DelegateAPI by setting a +config_group:: + + class MyCustomAPI(BaseDelegateAPI): + + url = r'^custom/mycoolstuff/?$' + + config_group = groups.DynamicNameConfigGroup( + children=[ + fields.StrConfig( + 'myconfig', + help_text="Some custom config.", + required=True, + default="Stuff", + ), + ] + ) + Building Tasks ============== -Tasks must be derived from adjutant.tasks.v1.base.BaseTask and can be -registered as such:: - - from adjutant.plugins import register_plugin_task - - register_plugin_task(MyPluginTask) - -Examples of tasks can be found in `adjutant.tasks.v1` +Tasks must be derived from ``adjutant.tasks.v1.base.BaseTask``. Examples +of tasks can be found in ``adjutant.tasks.v1`` Minimally task should define their required fields:: @@ -70,21 +138,32 @@ Minimally task should define their required fields:: ] duplicate_policy = "cancel" # default is cancel +Then there are other optional values you can set:: + + class My(MyPluginTask): + .... + + # previous task_types + deprecated_task_types = ['create_project'] + + # config defaults for the task (used to generate default config): + allow_auto_approve = True + additional_actions = None + token_expiry = None + action_config = None + email_config = None + notification_config = None + Building Actions ================ -Actions must be derived from adjutant.actions.v1.base.BaseAction and are -registered alongside their serializer:: - - from adjutant.plugins import register_plugin_action - - register_action_class(MyCustomAction, MyCustomActionSerializer) +Actions must be derived from ``adjutant.actions.v1.base.BaseAction``. Serializers can inherit from either rest_framework.serializers.Serializer, or the current serializers in adjutant.actions.v1.serializers. -Examples of actions can be found in `adjutant.actions.v1` +Examples of actions can be found in ``adjutant.actions.v1`` Minimally actions should define their required fields and implement 3 functions:: @@ -96,6 +175,8 @@ functions:: 'value1', ] + serializer = MyCustomActionSerializer + def _prepare(self): # Do some validation here pass @@ -109,7 +190,8 @@ functions:: self.add_note("Submit action performed") Information set in the action task cache is available in email templates under -task.cache.value, and the action data is available in action.ActionName.value. +``task.cache.value``, and the action data is available in +``action.ActionName.value``. If a token email is needed to be sent the action should also implement:: @@ -132,7 +214,7 @@ are django-rest-framework serializers, but there are also two base serializers available in adjutant.actions.v1.serializers, BaseUserNameSerializer and BaseUserIdSerializer. -All fields required for an action must be placed through the serializer +All fields required for an action must be plassed through the serializer otherwise they will be inaccessible to the action. Example:: @@ -154,7 +236,7 @@ Notification Handlers can also be added through a plugin:: class NewNotificationHandler(BaseNotificationHandler): - settings_group = groups.DynamicNameConfigGroup( + config_group = groups.DynamicNameConfigGroup( children=[ fields.BoolConfig( "do_this_thing", @@ -169,9 +251,6 @@ Notification Handlers can also be added through a plugin:: if conf.do_this_thing: # do something with the task and notification - - register_notification_handler(NewNotificationHandler) - You then need to setup the handler to be used either by default for a task, or for a specific task:: diff --git a/doc/source/features.rst b/doc/source/features.rst index 98f9091..9ba6751 100644 --- a/doc/source/features.rst +++ b/doc/source/features.rst @@ -11,9 +11,10 @@ handles. Adjutant does have default implementations of workflows and the APIs for them. These are in part meant to be workflow that is applicable to any cloud, but also example implementations, as well as actions that could potentially be -reused in deployer specific workflow in their own plugins. If anything could -be considered a feature, it potentially could be these. The plan is to add many -of these, which any cloud can use out of the box, or augment as needed. +reused in deployer specific workflow in their own feature sets. If anything +could be considered a feature, it potentially could be these. The plan is to +add many of these, which any cloud can use out of the box, or augment as +needed. To enable these they must be added to `ACTIVE_DELEGATE_APIS` in the conf file. diff --git a/doc/source/guide-lines.rst b/doc/source/guide-lines.rst index 448deef..a0615e6 100644 --- a/doc/source/guide-lines.rst +++ b/doc/source/guide-lines.rst @@ -9,9 +9,9 @@ Adjutant is a service to let cloud providers build workflow around certain actions, or to build smaller APIs around existing things in OpenStack. Or even APIs to integrate with OpenStack, but do actions in external systems. -Ultimately Adjutant is a Django project with a few limitations, and the plugin -system probably exposes too much extra functionality which can be added by a -plugin. Some of this we plan to cut down, and throw in some explicitly defined +Ultimately Adjutant is a Django project with a few limitations, and the feature +set system probably exposes too much extra functionality which can be added. +Some of this we plan to cut down, and throw in some explicitly defined limitations, but even with the planned limitations the framework will always be very flexible. @@ -58,14 +58,14 @@ wrappers or supplementary logic around existing OpenStack APIs and features. .. note:: - If an action, task, or API doesn't fit in core, it may fit in a plugin, - potentially even one that is maintained by the core team. If a feature isn't + If an action, task, or API doesn't fit in core, it may fit in a external feature + set, potentially even one that is maintained by the core team. If a feature isn't yet present in OpenStack that we can build in Adjutant quickly, we can do so - as a semi-official plugin with the knowledge that we plan to deprecate that + as a semi-official feature set with the knowledge that we plan to deprecate that feature when it becomes present in OpenStack proper. In addition this process allows us to potentially allow providers to expose a variant of the feature if they are running older versions of OpenStack that don't entirely support - it, but Adjutant could via the plugin mechanism. This gives us a large amount + it, but Adjutant could via the feature set mechanism. This gives us a large amount of flexibility, while ensuring we aren't reinventing the wheel. @@ -97,7 +97,7 @@ clean, and the changes auditable. .. warning:: - Anyone writing API plugins that break the above convention will not be + Anyone writing feature sets that break the above convention will not be supported. We may help and encourage you to move to using the underlying workflows, but the core team won't help you troubleshoot any logic that isn't in the right place. diff --git a/doc/source/index.rst b/doc/source/index.rst index e9d84b1..87315e4 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -9,7 +9,7 @@ Welcome to Adjutant's documentation! release-notes devstack-guide configuration - plugins + feature-sets quota guide-lines features diff --git a/etc/adjutant.yaml b/etc/adjutant.yaml index 9c576f1..dff18a2 100644 --- a/etc/adjutant.yaml +++ b/etc/adjutant.yaml @@ -9,9 +9,6 @@ django: # The Django allowed hosts allowed_hosts: - '*' - # List - # A list of additional django apps. - # additional_apps: # Dict # Django databases config. databases: @@ -272,11 +269,6 @@ workflow: # List # Roles which those users should get. # default_roles: - ResetUserPasswordAction: - # List - # Users with these roles cannot reset their passwords. - blacklisted_roles: - - admin NewDefaultNetworkAction: region_defaults: # String @@ -341,6 +333,11 @@ workflow: # Integer # The allowed number of days between auto approved quota changes. days_between_autoapprove: 30 + ResetUserPasswordAction: + # List + # Users with these roles cannot reset their passwords. + blacklisted_roles: + - admin SendAdditionalEmailAction: prepare: # String diff --git a/releasenotes/notes/feature-sets-f363d132c8c377cf.yaml b/releasenotes/notes/feature-sets-f363d132c8c377cf.yaml new file mode 100644 index 0000000..47df9cd --- /dev/null +++ b/releasenotes/notes/feature-sets-f363d132c8c377cf.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + Feature sets have been introduced, allowing Adjutant's plugins to be + registered via entrypoints, so all that is required to include them + is to install them in the same environment. Then which DelegateAPIs + are enabled from the feature sets is still controlled by + ``adjutant.api.active_delegate_apis``. +upgrade: + - | + Plugins that want to work with Adjutant will need to be upgraded to use + the new feature set pattern for registrations of Actions, Tasks, DelegateAPIs, + and NotificationHandlers. +deprecations: + - | + Adjutant's plugin mechanism has entirely changed, making many plugins + imcompatible until updated to match the new plugin mechanism. diff --git a/setup.cfg b/setup.cfg index d34a9d0..2b309a9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,3 +38,6 @@ packages = [entry_points] console_scripts = adjutant-api = adjutant:management_command + +adjutant.feature_sets = + core = adjutant.core:AdjutantCore