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):
|
def get_action(self):
|
||||||
"""Returns self as the appropriate action wrapper type."""
|
"""Returns self as the appropriate action wrapper type."""
|
||||||
data = self.action_data
|
data = self.action_data
|
||||||
return actions.ACTION_CLASSES[self.action_name][0](
|
return actions.ACTION_CLASSES[self.action_name](
|
||||||
data=data, action_model=self)
|
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 = []
|
required = []
|
||||||
|
|
||||||
|
serializer = None
|
||||||
|
|
||||||
config_group = None
|
config_group = None
|
||||||
|
|
||||||
def __init__(self, data, action_model=None, task=None,
|
def __init__(self, data, action_model=None, task=None,
|
||||||
|
@ -19,6 +19,7 @@ from confspirator import fields
|
|||||||
from confspirator import types
|
from confspirator import types
|
||||||
|
|
||||||
from adjutant.actions.v1.base import BaseAction
|
from adjutant.actions.v1.base import BaseAction
|
||||||
|
from adjutant.actions.v1 import serializers
|
||||||
from adjutant.actions.utils import send_email
|
from adjutant.actions.utils import send_email
|
||||||
from adjutant.common import user_store
|
from adjutant.common import user_store
|
||||||
from adjutant.common import constants
|
from adjutant.common import constants
|
||||||
@ -95,6 +96,8 @@ def _build_default_email_group(group_name):
|
|||||||
|
|
||||||
class SendAdditionalEmailAction(BaseAction):
|
class SendAdditionalEmailAction(BaseAction):
|
||||||
|
|
||||||
|
serializer = serializers.SendAdditionalEmailSerializer
|
||||||
|
|
||||||
config_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
_build_default_email_group("prepare"),
|
_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 uuid import uuid4
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from confspirator import groups
|
from confspirator import groups
|
||||||
from confspirator import fields
|
from confspirator import fields
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from adjutant.config import CONF
|
from adjutant.config import CONF
|
||||||
from adjutant.common import user_store
|
from adjutant.common import user_store
|
||||||
from adjutant.common.utils import str_datetime
|
from adjutant.common.utils import str_datetime
|
||||||
from adjutant.actions.utils import validate_steps
|
from adjutant.actions.utils import validate_steps
|
||||||
from adjutant.actions.v1.base import (
|
from adjutant.actions.v1.base import (
|
||||||
BaseAction, UserNameAction, UserMixin, ProjectMixin)
|
BaseAction, UserNameAction, UserMixin, ProjectMixin)
|
||||||
|
from adjutant.actions.v1 import serializers
|
||||||
|
|
||||||
|
|
||||||
class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
|
class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||||
@ -41,6 +42,8 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
|
|||||||
'description',
|
'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
serializer = serializers.NewProjectSerializer
|
||||||
|
|
||||||
config_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
fields.ListConfig(
|
fields.ListConfig(
|
||||||
@ -149,6 +152,8 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
|||||||
'email'
|
'email'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
serializer = serializers.NewProjectWithUserSerializer
|
||||||
|
|
||||||
config_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
fields.ListConfig(
|
fields.ListConfig(
|
||||||
@ -439,6 +444,8 @@ class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
|
|||||||
'domain_id',
|
'domain_id',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
serializer = serializers.AddDefaultUsersToProjectSerializer
|
||||||
|
|
||||||
config_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
fields.ListConfig(
|
fields.ListConfig(
|
||||||
|
@ -20,6 +20,7 @@ from confspirator import groups
|
|||||||
from confspirator import fields
|
from confspirator import fields
|
||||||
|
|
||||||
from adjutant.actions.v1.base import BaseAction, ProjectMixin, QuotaMixin
|
from adjutant.actions.v1.base import BaseAction, ProjectMixin, QuotaMixin
|
||||||
|
from adjutant.actions.v1 import serializers
|
||||||
from adjutant.actions.utils import validate_steps
|
from adjutant.actions.utils import validate_steps
|
||||||
from adjutant.common import openstack_clients, user_store
|
from adjutant.common import openstack_clients, user_store
|
||||||
from adjutant.api import models
|
from adjutant.api import models
|
||||||
@ -40,6 +41,8 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
|||||||
'region',
|
'region',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
serializer = serializers.NewDefaultNetworkSerializer
|
||||||
|
|
||||||
config_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
groups.ConfigGroup(
|
groups.ConfigGroup(
|
||||||
@ -232,6 +235,8 @@ class NewProjectDefaultNetworkAction(NewDefaultNetworkAction):
|
|||||||
'region',
|
'region',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
serializer = serializers.NewProjectDefaultNetworkSerializer
|
||||||
|
|
||||||
def _pre_validate(self):
|
def _pre_validate(self):
|
||||||
# Note: Don't check project here as it doesn't exist yet.
|
# Note: Don't check project here as it doesn't exist yet.
|
||||||
self.action.valid = validate_steps([
|
self.action.valid = validate_steps([
|
||||||
@ -266,6 +271,8 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
|||||||
'regions',
|
'regions',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
serializer = serializers.UpdateProjectQuotasSerializer
|
||||||
|
|
||||||
config_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
fields.FloatConfig(
|
fields.FloatConfig(
|
||||||
@ -429,6 +436,8 @@ class SetProjectQuotaAction(UpdateProjectQuotasAction):
|
|||||||
""" Updates quota for a given project to a configured quota level """
|
""" Updates quota for a given project to a configured quota level """
|
||||||
required = []
|
required = []
|
||||||
|
|
||||||
|
serializer = serializers.SetProjectQuotaSerializer
|
||||||
|
|
||||||
config_group = UpdateProjectQuotasAction.config_group.extend(
|
config_group = UpdateProjectQuotasAction.config_group.extend(
|
||||||
children=[
|
children=[
|
||||||
fields.DictConfig(
|
fields.DictConfig(
|
||||||
|
@ -80,7 +80,7 @@ class NewProjectWithUserSerializer(BaseUserNameSerializer):
|
|||||||
project_name = serializers.CharField(max_length=64)
|
project_name = serializers.CharField(max_length=64)
|
||||||
|
|
||||||
|
|
||||||
class ResetUserSerializer(BaseUserNameSerializer):
|
class ResetUserPasswordSerializer(BaseUserNameSerializer):
|
||||||
domain_name = serializers.CharField(max_length=64, default='Default')
|
domain_name = serializers.CharField(max_length=64, default='Default')
|
||||||
# override domain_id so serializer doesn't set it up.
|
# override domain_id so serializer doesn't set it up.
|
||||||
domain_id = None
|
domain_id = None
|
||||||
|
@ -19,6 +19,7 @@ from adjutant.config import CONF
|
|||||||
from adjutant.common import user_store
|
from adjutant.common import user_store
|
||||||
from adjutant.actions.v1.base import (
|
from adjutant.actions.v1.base import (
|
||||||
UserNameAction, UserIdAction, UserMixin, ProjectMixin)
|
UserNameAction, UserIdAction, UserMixin, ProjectMixin)
|
||||||
|
from adjutant.actions.v1 import serializers
|
||||||
from adjutant.actions.utils import validate_steps
|
from adjutant.actions.utils import validate_steps
|
||||||
|
|
||||||
|
|
||||||
@ -39,6 +40,8 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
|
|||||||
'domain_id',
|
'domain_id',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
serializer = serializers.NewUserSerializer
|
||||||
|
|
||||||
def _validate_target_user(self):
|
def _validate_target_user(self):
|
||||||
id_manager = user_store.IdentityManager()
|
id_manager = user_store.IdentityManager()
|
||||||
|
|
||||||
@ -181,6 +184,8 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
|
|||||||
'email'
|
'email'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
serializer = serializers.ResetUserPasswordSerializer
|
||||||
|
|
||||||
config_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
fields.ListConfig(
|
fields.ListConfig(
|
||||||
@ -267,6 +272,8 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
|
|||||||
'remove'
|
'remove'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
serializer = serializers.EditUserRolesSerializer
|
||||||
|
|
||||||
def _validate_target_user(self):
|
def _validate_target_user(self):
|
||||||
# Get target user
|
# Get target user
|
||||||
user = self._get_target_user()
|
user = self._get_target_user()
|
||||||
@ -403,6 +410,8 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
|
|||||||
'new_email',
|
'new_email',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
serializer = serializers.UpdateUserEmailSerializer
|
||||||
|
|
||||||
def _get_email(self):
|
def _get_email(self):
|
||||||
# Sending to new email address
|
# Sending to new email address
|
||||||
return self.new_email
|
return self.new_email
|
||||||
|
@ -12,24 +12,23 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from rest_framework_swagger.views import get_swagger_view
|
from rest_framework_swagger.views import get_swagger_view
|
||||||
|
|
||||||
from adjutant.api import views
|
from adjutant.api import views
|
||||||
|
from adjutant.api.views import build_version_details
|
||||||
from adjutant.api.v1 import views as views_v1
|
from adjutant.api.v1 import views as views_v1
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', views.VersionView.as_view()),
|
url(r'^$', views.VersionView.as_view()),
|
||||||
]
|
]
|
||||||
|
|
||||||
# NOTE(adriant): This may not be the best approach, but it does work. Will
|
# NOTE(adriant): make this conditional once we have a v2.
|
||||||
# gladly accept a cleaner alternative if it presents itself.
|
build_version_details('1.0', 'CURRENT', relative_endpoint='v1/')
|
||||||
if apps.is_installed('adjutant.api.v1'):
|
urlpatterns.append(url(r'^v1/?$', views_v1.V1VersionEndpoint.as_view()))
|
||||||
urlpatterns.append(url(r'^v1/?$', views_v1.V1VersionEndpoint.as_view()))
|
urlpatterns.append(url(r'^v1/', include('adjutant.api.v1.urls')))
|
||||||
urlpatterns.append(url(r'^v1/', include('adjutant.api.v1.urls')))
|
|
||||||
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
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):
|
class BaseDelegateAPI(APIViewWithLogger):
|
||||||
"""Base Class for Adjutant's deployer configurable APIs."""
|
"""Base Class for Adjutant's deployer configurable APIs."""
|
||||||
|
|
||||||
|
url = None
|
||||||
|
|
||||||
config_group = None
|
config_group = None
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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):
|
class UserList(tasks.InviteUser):
|
||||||
|
|
||||||
|
url = r'^openstack/users/?$'
|
||||||
|
|
||||||
config_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
fields.ListConfig(
|
fields.ListConfig(
|
||||||
@ -168,6 +170,8 @@ class UserList(tasks.InviteUser):
|
|||||||
|
|
||||||
class UserDetail(BaseDelegateAPI):
|
class UserDetail(BaseDelegateAPI):
|
||||||
|
|
||||||
|
url = r'^openstack/users/(?P<user_id>\w+)/?$'
|
||||||
|
|
||||||
config_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
fields.ListConfig(
|
fields.ListConfig(
|
||||||
@ -244,6 +248,8 @@ class UserDetail(BaseDelegateAPI):
|
|||||||
|
|
||||||
class UserRoles(BaseDelegateAPI):
|
class UserRoles(BaseDelegateAPI):
|
||||||
|
|
||||||
|
url = r'^openstack/users/(?P<user_id>\w+)/roles/?$'
|
||||||
|
|
||||||
config_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
fields.ListConfig(
|
fields.ListConfig(
|
||||||
@ -317,6 +323,8 @@ class UserRoles(BaseDelegateAPI):
|
|||||||
|
|
||||||
class RoleList(BaseDelegateAPI):
|
class RoleList(BaseDelegateAPI):
|
||||||
|
|
||||||
|
url = r'^openstack/roles/?$'
|
||||||
|
|
||||||
@utils.mod_or_admin
|
@utils.mod_or_admin
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Returns a list of roles that may be managed for this project"""
|
"""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
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -352,6 +362,8 @@ class UserUpdateEmail(tasks.UpdateEmail):
|
|||||||
---
|
---
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
url = r'^openstack/users/email-update/?$'
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -360,6 +372,8 @@ class SignUp(tasks.CreateProjectAndUser):
|
|||||||
The openstack endpoint for signups.
|
The openstack endpoint for signups.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
url = r'^openstack/sign-up/?$'
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -369,6 +383,8 @@ class UpdateProjectQuotas(BaseDelegateAPI):
|
|||||||
one or more regions
|
one or more regions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
url = r'^openstack/quotas/?$'
|
||||||
|
|
||||||
task_type = "update_quota"
|
task_type = "update_quota"
|
||||||
|
|
||||||
_number_of_returned_tasks = 5
|
_number_of_returned_tasks = 5
|
||||||
|
@ -29,6 +29,8 @@ from adjutant.api.v1.base import BaseDelegateAPI
|
|||||||
|
|
||||||
class CreateProjectAndUser(BaseDelegateAPI):
|
class CreateProjectAndUser(BaseDelegateAPI):
|
||||||
|
|
||||||
|
url = r'^actions/CreateProjectAndUser/?$'
|
||||||
|
|
||||||
config_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
fields.StrConfig(
|
fields.StrConfig(
|
||||||
@ -83,6 +85,8 @@ class CreateProjectAndUser(BaseDelegateAPI):
|
|||||||
|
|
||||||
class InviteUser(BaseDelegateAPI):
|
class InviteUser(BaseDelegateAPI):
|
||||||
|
|
||||||
|
url = r'^actions/InviteUser/?$'
|
||||||
|
|
||||||
task_type = "invite_user_to_project"
|
task_type = "invite_user_to_project"
|
||||||
|
|
||||||
@utils.mod_or_admin
|
@utils.mod_or_admin
|
||||||
@ -118,6 +122,8 @@ class InviteUser(BaseDelegateAPI):
|
|||||||
|
|
||||||
class ResetPassword(BaseDelegateAPI):
|
class ResetPassword(BaseDelegateAPI):
|
||||||
|
|
||||||
|
url = r'^actions/ResetPassword/?$'
|
||||||
|
|
||||||
task_type = "reset_user_password"
|
task_type = "reset_user_password"
|
||||||
|
|
||||||
@utils.minimal_duration(min_time=3)
|
@utils.minimal_duration(min_time=3)
|
||||||
@ -160,6 +166,8 @@ class ResetPassword(BaseDelegateAPI):
|
|||||||
|
|
||||||
class EditUser(BaseDelegateAPI):
|
class EditUser(BaseDelegateAPI):
|
||||||
|
|
||||||
|
url = r'^actions/EditUser/?$'
|
||||||
|
|
||||||
task_type = "edit_user_roles"
|
task_type = "edit_user_roles"
|
||||||
|
|
||||||
@utils.mod_or_admin
|
@utils.mod_or_admin
|
||||||
@ -179,6 +187,9 @@ class EditUser(BaseDelegateAPI):
|
|||||||
|
|
||||||
|
|
||||||
class UpdateEmail(BaseDelegateAPI):
|
class UpdateEmail(BaseDelegateAPI):
|
||||||
|
|
||||||
|
url = r'^actions/UpdateEmail/?$'
|
||||||
|
|
||||||
task_type = "update_user_email"
|
task_type = "update_user_email"
|
||||||
|
|
||||||
@utils.authenticated
|
@utils.authenticated
|
||||||
|
@ -33,5 +33,5 @@ for active_view in CONF.api.active_delegate_apis:
|
|||||||
delegate_api = api.DELEGATE_API_CLASSES[active_view]
|
delegate_api = api.DELEGATE_API_CLASSES[active_view]
|
||||||
|
|
||||||
urlpatterns.append(
|
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,
|
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(
|
config_group.register_child_config(
|
||||||
fields.DictConfig(
|
fields.DictConfig(
|
||||||
"databases",
|
"databases",
|
||||||
|
@ -15,4 +15,4 @@
|
|||||||
from confspirator import groups
|
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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from logging import getLogger
|
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
@ -25,56 +24,11 @@ from confspirator import types
|
|||||||
|
|
||||||
from adjutant.config import CONF
|
from adjutant.config import CONF
|
||||||
from adjutant.common import constants
|
from adjutant.common import constants
|
||||||
from adjutant import notifications
|
|
||||||
from adjutant.api.models import Notification
|
from adjutant.api.models import Notification
|
||||||
from adjutant import exceptions
|
from adjutant.notifications.v1 import base
|
||||||
from adjutant.config.notification import handler_defaults_group
|
|
||||||
|
|
||||||
|
|
||||||
class BaseNotificationHandler(object):
|
class EmailNotification(base.BaseNotificationHandler):
|
||||||
""""""
|
|
||||||
|
|
||||||
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):
|
|
||||||
"""
|
"""
|
||||||
Basic email notification handler. Will
|
Basic email notification handler. Will
|
||||||
send an email with the given templates.
|
send an email with the given templates.
|
||||||
@ -186,23 +140,3 @@ class EmailNotification(BaseNotificationHandler):
|
|||||||
task=notification.task, notes=notes, error=True
|
task=notification.task, notes=notes, error=True
|
||||||
)
|
)
|
||||||
error_notification.save()
|
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 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 (
|
from adjutant.common.tests.fake_clients import (
|
||||||
FakeManager, setup_identity_cache)
|
FakeManager, setup_identity_cache)
|
||||||
from adjutant.common.tests.utils import AdjutantAPITestCase
|
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.api',
|
||||||
'adjutant.notifications',
|
'adjutant.notifications',
|
||||||
'adjutant.tasks',
|
'adjutant.tasks',
|
||||||
|
'adjutant.startup',
|
||||||
# NOTE(adriant): Until we have v2 options, hardcode our v1s
|
|
||||||
'adjutant.actions.v1',
|
|
||||||
'adjutant.tasks.v1',
|
|
||||||
'adjutant.api.v1',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = (
|
MIDDLEWARE_CLASSES = (
|
||||||
@ -123,12 +119,6 @@ if DEBUG:
|
|||||||
|
|
||||||
ALLOWED_HOSTS = adj_conf.django.allowed_hosts
|
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
|
DATABASES = adj_conf.django.databases
|
||||||
|
|
||||||
if adj_conf.django.logging:
|
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.config import CONF
|
||||||
from adjutant import actions, api, tasks
|
from adjutant import actions, api, tasks
|
||||||
@ -34,22 +46,3 @@ def check_configured_actions():
|
|||||||
if missing_actions:
|
if missing_actions:
|
||||||
raise ActionNotRegistered(
|
raise ActionNotRegistered(
|
||||||
"Configured actions are unregistered: %s" % missing_actions)
|
"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:
|
else:
|
||||||
action_name = action
|
action_name = action
|
||||||
|
|
||||||
action_class, serializer_class = \
|
action_class = adj_actions.ACTION_CLASSES[action_name]
|
||||||
adj_actions.ACTION_CLASSES[action_name]
|
|
||||||
|
|
||||||
if use_existing_actions:
|
if use_existing_actions:
|
||||||
action_class = action
|
action_class = action
|
||||||
|
|
||||||
# instantiate serializer class
|
# instantiate serializer class
|
||||||
if not serializer_class:
|
if not action_class.serializer:
|
||||||
raise exceptions.SerializerMissingException(
|
raise exceptions.SerializerMissingException(
|
||||||
"No serializer defined for action %s" % action_name)
|
"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({
|
action_serializer_list.append({
|
||||||
'name': action_name,
|
'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):
|
class CreateProjectAndUser(BaseTask):
|
||||||
duplicate_policy = "block"
|
duplicate_policy = "block"
|
||||||
task_type = "create_project_and_user"
|
task_type = "create_project_and_user"
|
||||||
deprecated_task_types = ['create_project']
|
deprecated_task_types = ['create_project', 'signup']
|
||||||
default_actions = [
|
default_actions = [
|
||||||
"NewProjectWithUserAction",
|
"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,
|
three stages, and there is even the ability to chain multiple actions together,
|
||||||
and pass data along to other actions.
|
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.
|
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
|
Adjutant supports the introduction of new Actions, Tasks, and DelegateAPIs
|
||||||
apps system to allow us a plugin mechanism that allows additional actions and
|
via additional feature sets. A feature set is a bundle of these elements
|
||||||
views to be brought in via external sources. This allows company specific or
|
with maybe some feature set specific extra config. This allows company specific
|
||||||
deployer specific changes to easily live outside of the core service and simply
|
or deployer specific changes to easily live outside of the core service and
|
||||||
extend the core service where and when need.
|
simply extend the core service where and when need.
|
||||||
|
|
||||||
An example of such a plugin is here:
|
An example of such a plugin is here (although it may not yet be using the new
|
||||||
https://github.com/catalyst/adjutant-odoo
|
'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
|
Building DelegateAPIs
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
New DelegateAPIs should inherit from adjutant.api.v1.base.BaseDelegateAPI
|
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.
|
|
||||||
|
|
||||||
A new DelegateAPI from a plugin can effectively 'override' a default
|
A new DelegateAPI from a plugin can effectively 'override' a default
|
||||||
DelegateAPI by registering with the same URL. However it must have
|
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::
|
Minimally they can look like this::
|
||||||
|
|
||||||
class NewCreateProject(BaseDelegateAPI):
|
class MyCustomAPI(BaseDelegateAPI):
|
||||||
|
|
||||||
|
url = r'^custom/mycoolstuff/?$'
|
||||||
|
|
||||||
@utils.authenticated
|
@utils.authenticated
|
||||||
def post(self, request):
|
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
|
standard django view handlers and can execute any needed code. Additional
|
||||||
information for the task should be placed in request.data.
|
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
|
Building Tasks
|
||||||
==============
|
==============
|
||||||
|
|
||||||
Tasks must be derived from adjutant.tasks.v1.base.BaseTask and can be
|
Tasks must be derived from ``adjutant.tasks.v1.base.BaseTask``. Examples
|
||||||
registered as such::
|
of tasks can be found in ``adjutant.tasks.v1``
|
||||||
|
|
||||||
from adjutant.plugins import register_plugin_task
|
|
||||||
|
|
||||||
register_plugin_task(MyPluginTask)
|
|
||||||
|
|
||||||
Examples of tasks can be found in `adjutant.tasks.v1`
|
|
||||||
|
|
||||||
Minimally task should define their required fields::
|
Minimally task should define their required fields::
|
||||||
|
|
||||||
@ -70,21 +138,32 @@ Minimally task should define their required fields::
|
|||||||
]
|
]
|
||||||
duplicate_policy = "cancel" # default is cancel
|
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
|
Building Actions
|
||||||
================
|
================
|
||||||
|
|
||||||
Actions must be derived from adjutant.actions.v1.base.BaseAction and are
|
Actions must be derived from ``adjutant.actions.v1.base.BaseAction``.
|
||||||
registered alongside their serializer::
|
|
||||||
|
|
||||||
from adjutant.plugins import register_plugin_action
|
|
||||||
|
|
||||||
register_action_class(MyCustomAction, MyCustomActionSerializer)
|
|
||||||
|
|
||||||
Serializers can inherit from either rest_framework.serializers.Serializer, or
|
Serializers can inherit from either rest_framework.serializers.Serializer, or
|
||||||
the current serializers in adjutant.actions.v1.serializers.
|
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
|
Minimally actions should define their required fields and implement 3
|
||||||
functions::
|
functions::
|
||||||
@ -96,6 +175,8 @@ functions::
|
|||||||
'value1',
|
'value1',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
serializer = MyCustomActionSerializer
|
||||||
|
|
||||||
def _prepare(self):
|
def _prepare(self):
|
||||||
# Do some validation here
|
# Do some validation here
|
||||||
pass
|
pass
|
||||||
@ -109,7 +190,8 @@ functions::
|
|||||||
self.add_note("Submit action performed")
|
self.add_note("Submit action performed")
|
||||||
|
|
||||||
Information set in the action task cache is available in email templates under
|
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::
|
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
|
available in adjutant.actions.v1.serializers, BaseUserNameSerializer and
|
||||||
BaseUserIdSerializer.
|
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.
|
otherwise they will be inaccessible to the action.
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
@ -154,7 +236,7 @@ Notification Handlers can also be added through a plugin::
|
|||||||
|
|
||||||
class NewNotificationHandler(BaseNotificationHandler):
|
class NewNotificationHandler(BaseNotificationHandler):
|
||||||
|
|
||||||
settings_group = groups.DynamicNameConfigGroup(
|
config_group = groups.DynamicNameConfigGroup(
|
||||||
children=[
|
children=[
|
||||||
fields.BoolConfig(
|
fields.BoolConfig(
|
||||||
"do_this_thing",
|
"do_this_thing",
|
||||||
@ -169,9 +251,6 @@ Notification Handlers can also be added through a plugin::
|
|||||||
if conf.do_this_thing:
|
if conf.do_this_thing:
|
||||||
# do something with the task and notification
|
# 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,
|
You then need to setup the handler to be used either by default for a task,
|
||||||
or for a specific task::
|
or for a specific task::
|
||||||
|
|
@ -11,9 +11,10 @@ handles.
|
|||||||
Adjutant does have default implementations of workflows and the APIs for
|
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,
|
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
|
but also example implementations, as well as actions that could potentially be
|
||||||
reused in deployer specific workflow in their own plugins. If anything could
|
reused in deployer specific workflow in their own feature sets. If anything
|
||||||
be considered a feature, it potentially could be these. The plan is to add many
|
could be considered a feature, it potentially could be these. The plan is to
|
||||||
of these, which any cloud can use out of the box, or augment as needed.
|
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.
|
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
|
actions, or to build smaller APIs around existing things in OpenStack. Or even
|
||||||
APIs to integrate with OpenStack, but do actions in external systems.
|
APIs to integrate with OpenStack, but do actions in external systems.
|
||||||
|
|
||||||
Ultimately Adjutant is a Django project with a few limitations, and the plugin
|
Ultimately Adjutant is a Django project with a few limitations, and the feature
|
||||||
system probably exposes too much extra functionality which can be added by a
|
set system probably exposes too much extra functionality which can be added.
|
||||||
plugin. Some of this we plan to cut down, and throw in some explicitly defined
|
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
|
limitations, but even with the planned limitations the framework will always
|
||||||
be very flexible.
|
be very flexible.
|
||||||
|
|
||||||
@ -58,14 +58,14 @@ wrappers or supplementary logic around existing OpenStack APIs and features.
|
|||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
If an action, task, or API doesn't fit in core, it may fit in a plugin,
|
If an action, task, or API doesn't fit in core, it may fit in a external feature
|
||||||
potentially even one that is maintained by the core team. If a feature isn't
|
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
|
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
|
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
|
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
|
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.
|
of flexibility, while ensuring we aren't reinventing the wheel.
|
||||||
|
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ clean, and the changes auditable.
|
|||||||
|
|
||||||
.. warning::
|
.. 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
|
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
|
workflows, but the core team won't help you troubleshoot any logic that isn't
|
||||||
in the right place.
|
in the right place.
|
||||||
|
@ -9,7 +9,7 @@ Welcome to Adjutant's documentation!
|
|||||||
release-notes
|
release-notes
|
||||||
devstack-guide
|
devstack-guide
|
||||||
configuration
|
configuration
|
||||||
plugins
|
feature-sets
|
||||||
quota
|
quota
|
||||||
guide-lines
|
guide-lines
|
||||||
features
|
features
|
||||||
|
@ -9,9 +9,6 @@ django:
|
|||||||
# The Django allowed hosts
|
# The Django allowed hosts
|
||||||
allowed_hosts:
|
allowed_hosts:
|
||||||
- '*'
|
- '*'
|
||||||
# List
|
|
||||||
# A list of additional django apps.
|
|
||||||
# additional_apps:
|
|
||||||
# Dict
|
# Dict
|
||||||
# Django databases config.
|
# Django databases config.
|
||||||
databases:
|
databases:
|
||||||
@ -272,11 +269,6 @@ workflow:
|
|||||||
# List
|
# List
|
||||||
# Roles which those users should get.
|
# Roles which those users should get.
|
||||||
# default_roles:
|
# default_roles:
|
||||||
ResetUserPasswordAction:
|
|
||||||
# List
|
|
||||||
# Users with these roles cannot reset their passwords.
|
|
||||||
blacklisted_roles:
|
|
||||||
- admin
|
|
||||||
NewDefaultNetworkAction:
|
NewDefaultNetworkAction:
|
||||||
region_defaults:
|
region_defaults:
|
||||||
# String
|
# String
|
||||||
@ -341,6 +333,11 @@ workflow:
|
|||||||
# Integer
|
# Integer
|
||||||
# The allowed number of days between auto approved quota changes.
|
# The allowed number of days between auto approved quota changes.
|
||||||
days_between_autoapprove: 30
|
days_between_autoapprove: 30
|
||||||
|
ResetUserPasswordAction:
|
||||||
|
# List
|
||||||
|
# Users with these roles cannot reset their passwords.
|
||||||
|
blacklisted_roles:
|
||||||
|
- admin
|
||||||
SendAdditionalEmailAction:
|
SendAdditionalEmailAction:
|
||||||
prepare:
|
prepare:
|
||||||
# String
|
# 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