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
This commit is contained in:
parent
c750fd6d6c
commit
0eaac89b38
@ -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)
|
||||
|
@ -1 +0,0 @@
|
||||
default_app_config = 'adjutant.actions.v1.app.ActionV1Config'
|
@ -1,7 +0,0 @@
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ActionV1Config(AppConfig):
|
||||
name = "adjutant.actions.v1"
|
||||
label = 'actions_v1'
|
@ -60,6 +60,8 @@ class BaseAction(object):
|
||||
|
||||
required = []
|
||||
|
||||
serializer = None
|
||||
|
||||
config_group = None
|
||||
|
||||
def __init__(self, data, action_model=None, task=None,
|
||||
|
@ -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"),
|
||||
|
@ -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:
|
||||
# (<ActionClass>, <ActionSerializer>)
|
||||
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)
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -1 +0,0 @@
|
||||
default_app_config = 'adjutant.api.v1.app.APIV1Config'
|
@ -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/')
|
@ -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):
|
||||
|
@ -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<user_id>\w+)/?$', openstack.UserDetail)
|
||||
register_delegate_api_class(
|
||||
r'^openstack/users/(?P<user_id>\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)
|
@ -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<user_id>\w+)/?$'
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
@ -244,6 +248,8 @@ class UserDetail(BaseDelegateAPI):
|
||||
|
||||
class UserRoles(BaseDelegateAPI):
|
||||
|
||||
url = r'^openstack/users/(?P<user_id>\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
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
)
|
||||
|
@ -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",
|
||||
|
@ -15,4 +15,4 @@
|
||||
from confspirator import groups
|
||||
|
||||
|
||||
config_group = groups.ConfigGroup("plugin")
|
||||
config_group = groups.ConfigGroup("feature_sets")
|
83
adjutant/core.py
Normal file
83
adjutant/core.py
Normal file
@ -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,
|
||||
]
|
176
adjutant/feature_set.py
Normal file
176
adjutant/feature_set.py
Normal file
@ -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)
|
60
adjutant/notifications/v1/base.py
Normal file
60
adjutant/notifications/v1/base.py
Normal file
@ -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
|
@ -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)
|
0
adjutant/notifications/v1/tests/__init__.py
Normal file
0
adjutant/notifications/v1/tests/__init__.py
Normal file
@ -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
|
@ -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)
|
@ -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:
|
||||
|
@ -1 +1 @@
|
||||
default_app_config = 'adjutant.startup.checks.StartUpConfig'
|
||||
default_app_config = 'adjutant.startup.config.StartUpConfig'
|
||||
|
@ -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()
|
||||
|
40
adjutant/startup/config.py
Normal file
40
adjutant/startup/config.py
Normal file
@ -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()
|
21
adjutant/startup/loading.py
Normal file
21
adjutant/startup/loading.py
Normal file
@ -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()
|
@ -1 +0,0 @@
|
||||
default_app_config = 'adjutant.tasks.v1.app.TasksV1Config'
|
@ -1,7 +0,0 @@
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TasksV1Config(AppConfig):
|
||||
name = "adjutant.tasks.v1"
|
||||
label = 'tasks_v1'
|
@ -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,
|
||||
|
@ -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)
|
@ -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",
|
||||
]
|
||||
|
@ -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.
|
||||
|
||||
|
||||
|
@ -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::
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -9,7 +9,7 @@ Welcome to Adjutant's documentation!
|
||||
release-notes
|
||||
devstack-guide
|
||||
configuration
|
||||
plugins
|
||||
feature-sets
|
||||
quota
|
||||
guide-lines
|
||||
features
|
||||
|
@ -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
|
||||
|
17
releasenotes/notes/feature-sets-f363d132c8c377cf.yaml
Normal file
17
releasenotes/notes/feature-sets-f363d132c8c377cf.yaml
Normal file
@ -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.
|
Loading…
x
Reference in New Issue
Block a user