Rework Adjutant's config system to use CONFspirator
CONFspirator was written to just specifically for Adjutant and it allows us to do oslo.config style config management and definition with nested groups and for yaml. This is a major change that touches vast amounts of the code simply because of how much the config touches. Actions, Tasks, DelegateAPIs, and Notification Handlers now can define config in their own class and this will be added to the config. All the other config is located in `adjutant.config`, with everything now registed nicely on the config tree, and grouped in much saner ways. CONFspirator will also now allow Adjutant to be entirely configured via environment variables. We have removed `modify_dict_settings` because that is now entirely handled by CONFspirator's test utils. `NotificationEngine`s are now `NotificationHandler`s. `test_settings.py` is gone! And we now have better ways to define test settings and defaults. Project line length bumped to 88, and bugbear added to enforce that instead. Story: 2004488 Change-Id: I1d97d72d06b3a3a5df90355d3a4b4fe414381424
This commit is contained in:
parent
c9038dfe69
commit
c750fd6d6c
@ -12,6 +12,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
|
||||
from confspirator.exceptions import InvalidConf
|
||||
|
||||
|
||||
def management_command():
|
||||
"""Entry-point for the 'adjutant' command-line admin utility."""
|
||||
@ -22,4 +26,9 @@ def management_command():
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
try:
|
||||
execute_from_command_line(sys.argv)
|
||||
except InvalidConf as e:
|
||||
print("This command requires a valid config, see following errors:")
|
||||
print(json.dumps(e.errors["adjutant"], indent=2))
|
||||
sys.exit(1)
|
||||
|
@ -0,0 +1,17 @@
|
||||
# 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.
|
||||
|
||||
# Dict of actions and their serializers.
|
||||
# - This is populated from the various model modules at startup:
|
||||
ACTION_CLASSES = {}
|
@ -14,10 +14,11 @@
|
||||
|
||||
from jsonfield import JSONField
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
from adjutant import actions
|
||||
|
||||
|
||||
class Action(models.Model):
|
||||
"""
|
||||
@ -45,5 +46,5 @@ class Action(models.Model):
|
||||
def get_action(self):
|
||||
"""Returns self as the appropriate action wrapper type."""
|
||||
data = self.action_data
|
||||
return settings.ACTION_CLASSES[self.action_name][0](
|
||||
return actions.ACTION_CLASSES[self.action_name][0](
|
||||
data=data, action_model=self)
|
||||
|
@ -1,11 +1,9 @@
|
||||
import six
|
||||
from smtplib import SMTPException
|
||||
|
||||
from adjutant.api.v1.utils import create_notification
|
||||
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template import loader
|
||||
from django.conf import settings
|
||||
|
||||
from adjutant.notifications.utils import create_notification
|
||||
|
||||
|
||||
def validate_steps(validation_steps):
|
||||
@ -46,7 +44,7 @@ def send_email(to_addresses, context, conf, task):
|
||||
conf['template'],
|
||||
using='include_etc_templates')
|
||||
|
||||
html_template = conf.get('html_template', None)
|
||||
html_template = conf.get('html_template')
|
||||
if html_template:
|
||||
html_template = loader.get_template(
|
||||
html_template,
|
||||
@ -89,25 +87,21 @@ def send_email(to_addresses, context, conf, task):
|
||||
email.send(fail_silently=False)
|
||||
return True
|
||||
|
||||
except SMTPException as e:
|
||||
except Exception as e:
|
||||
notes = {
|
||||
'errors':
|
||||
("Error: '%s' while sending additional email for task: %s"
|
||||
% (e, task.uuid))
|
||||
("Error: '%s' while sending additional email for task: %s" %
|
||||
(e, task.uuid))
|
||||
}
|
||||
|
||||
errors_conf = settings.TASK_SETTINGS.get(
|
||||
task.task_type, settings.DEFAULT_TASK_SETTINGS).get(
|
||||
'errors', {}).get("SMTPException", {})
|
||||
notif_conf = task.config.notifications
|
||||
|
||||
if errors_conf:
|
||||
if e.__class__.__name__ in notif_conf.safe_errors:
|
||||
notification = create_notification(
|
||||
task, notes, error=True,
|
||||
engines=errors_conf.get('engines', True))
|
||||
|
||||
if errors_conf.get('notification') == "acknowledge":
|
||||
notification.acknowledged = True
|
||||
notification.save()
|
||||
handlers=False)
|
||||
notification.acknowledged = True
|
||||
notification.save()
|
||||
else:
|
||||
create_notification(task, notes, error=True)
|
||||
|
||||
|
@ -14,9 +14,9 @@
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from adjutant.config import CONF
|
||||
from adjutant.common.quota import QuotaManager
|
||||
from adjutant.common import user_store
|
||||
from adjutant.common.utils import str_datetime
|
||||
@ -60,6 +60,8 @@ class BaseAction(object):
|
||||
|
||||
required = []
|
||||
|
||||
config_group = None
|
||||
|
||||
def __init__(self, data, action_model=None, task=None,
|
||||
order=None):
|
||||
"""
|
||||
@ -87,6 +89,11 @@ class BaseAction(object):
|
||||
action.save()
|
||||
self.action = action
|
||||
|
||||
# NOTE(adriant): override this since we don't need the group
|
||||
# beyond registration.
|
||||
self.config_group = None
|
||||
self._config = None
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
return self.action.valid
|
||||
@ -136,18 +143,28 @@ class BaseAction(object):
|
||||
str(self), note)
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
"""Get my settings.
|
||||
def config(self):
|
||||
"""Get my config.
|
||||
|
||||
Returns a dict of the settings for this action.
|
||||
Returns a config_group of the config for this action.
|
||||
"""
|
||||
if self._config is not None:
|
||||
return self._config
|
||||
|
||||
try:
|
||||
task_conf = settings.TASK_SETTINGS[self.action.task.task_type]
|
||||
return task_conf['action_settings'].get(
|
||||
self.__class__.__name__, {})
|
||||
action_defaults = CONF.workflow.action_defaults.get(
|
||||
self.__class__.__name__)
|
||||
except KeyError:
|
||||
return settings.DEFAULT_ACTION_SETTINGS.get(
|
||||
self.__class__.__name__, {})
|
||||
self._config = {}
|
||||
return self._config
|
||||
|
||||
try:
|
||||
task_conf = CONF.workflow.tasks[self.action.task.task_type]
|
||||
self._config = action_defaults.overlay(
|
||||
task_conf.actions[self.__class__.__name__])
|
||||
except KeyError:
|
||||
self._config = action_defaults
|
||||
return self._config
|
||||
|
||||
def prepare(self):
|
||||
try:
|
||||
@ -266,13 +283,13 @@ class UserMixin(ResourceMixin):
|
||||
def _validate_role_permissions(self):
|
||||
keystone_user = self.action.task.keystone_user
|
||||
# Role permissions check
|
||||
if not self.are_roles_managable(user_roles=keystone_user['roles'],
|
||||
requested_roles=self.roles):
|
||||
if not self.are_roles_manageable(user_roles=keystone_user['roles'],
|
||||
requested_roles=self.roles):
|
||||
self.add_note('User does not have permission to edit role(s).')
|
||||
return False
|
||||
return True
|
||||
|
||||
def are_roles_managable(self, user_roles=None, requested_roles=None):
|
||||
def are_roles_manageable(self, user_roles=None, requested_roles=None):
|
||||
if user_roles is None:
|
||||
user_roles = []
|
||||
if requested_roles is None:
|
||||
@ -280,13 +297,14 @@ class UserMixin(ResourceMixin):
|
||||
|
||||
requested_roles = set(requested_roles)
|
||||
# blacklist checks
|
||||
blacklist_roles = set(['admin'])
|
||||
if len(blacklist_roles & requested_roles) > 0:
|
||||
blacklisted_roles = set(['admin'])
|
||||
if len(blacklisted_roles & requested_roles) > 0:
|
||||
return False
|
||||
|
||||
# user managable role
|
||||
managable_roles = user_store.get_managable_roles(user_roles)
|
||||
intersection = set(managable_roles) & requested_roles
|
||||
# user manageable role
|
||||
id_manager = user_store.IdentityManager()
|
||||
manageable_roles = id_manager.get_manageable_roles(user_roles)
|
||||
intersection = set(manageable_roles) & requested_roles
|
||||
# if all requested roles match, we can proceed
|
||||
return intersection == requested_roles
|
||||
|
||||
@ -457,8 +475,8 @@ class QuotaMixin(ResourceMixin):
|
||||
def _usage_greater_than_quota(self, regions):
|
||||
quota_manager = QuotaManager(
|
||||
self.project_id,
|
||||
size_difference_threshold=self.size_difference_threshold)
|
||||
quota = settings.PROJECT_QUOTA_SIZES.get(self.size, {})
|
||||
size_difference_threshold=self.config.size_difference_threshold)
|
||||
quota = CONF.quota.sizes.get(self.size, {})
|
||||
for region in regions:
|
||||
current_usage = quota_manager.get_current_usage(region)
|
||||
if self._region_usage_greater_than_quota(current_usage, quota):
|
||||
@ -498,7 +516,7 @@ class UserNameAction(BaseAction):
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if settings.USERNAME_IS_EMAIL:
|
||||
if CONF.identity.username_is_email:
|
||||
# NOTE(amelia): Make a copy to avoid editing it globally.
|
||||
self.required = list(self.required)
|
||||
try:
|
||||
|
@ -14,20 +14,100 @@
|
||||
|
||||
import six
|
||||
|
||||
from django.conf import settings
|
||||
from confspirator import groups
|
||||
from confspirator import fields
|
||||
from confspirator import types
|
||||
|
||||
from adjutant.actions.v1.base import BaseAction
|
||||
from adjutant.common import user_store
|
||||
from adjutant.actions.utils import send_email
|
||||
from adjutant.common import user_store
|
||||
from adjutant.common import constants
|
||||
from adjutant.config import CONF
|
||||
|
||||
|
||||
def _build_default_email_group(group_name):
|
||||
email_group = groups.ConfigGroup(group_name)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"subject",
|
||||
help_text="Email subject for this stage.",
|
||||
default="Openstack Email Notification")
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"from",
|
||||
help_text="From email for this stage.",
|
||||
regex=constants.EMAIL_WITH_TEMPLATE_REGEX,
|
||||
default="bounce+%(task_uuid)s@example.com")
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"reply",
|
||||
help_text="Reply-to email for this stage.",
|
||||
regex=constants.EMAIL_WITH_TEMPLATE_REGEX,
|
||||
default="no-reply@example.com")
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"template",
|
||||
help_text="Email template for this stage. "
|
||||
"No template will cause the email not to send.",
|
||||
default=None)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"html_template",
|
||||
help_text="Email html template for this stage. "
|
||||
"No template will cause the email not to send.",
|
||||
default=None)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.BoolConfig(
|
||||
"email_current_user",
|
||||
help_text="Email the user who started the task.",
|
||||
default=False,
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.BoolConfig(
|
||||
"email_task_cache",
|
||||
help_text="Send to an email set in the task cache.",
|
||||
default=False,
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.ListConfig(
|
||||
"email_roles",
|
||||
help_text="Send emails to the given roles on the project.",
|
||||
default=[],
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.ListConfig(
|
||||
"email_additional_addresses",
|
||||
help_text="Send emails to an arbitrary admin emails",
|
||||
item_type=types.String(regex=constants.EMAIL_WITH_TEMPLATE_REGEX),
|
||||
default=[],
|
||||
)
|
||||
)
|
||||
return email_group
|
||||
|
||||
|
||||
class SendAdditionalEmailAction(BaseAction):
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
_build_default_email_group("prepare"),
|
||||
_build_default_email_group("approve"),
|
||||
_build_default_email_group("submit"),
|
||||
],
|
||||
)
|
||||
|
||||
def set_email(self, conf):
|
||||
self.emails = set()
|
||||
if conf.get('email_current_user'):
|
||||
self.add_note("Adding the current user's email address")
|
||||
if settings.USERNAME_IS_EMAIL:
|
||||
if CONF.identity.username_is_email:
|
||||
self.emails.add(self.action.task.keystone_user['username'])
|
||||
else:
|
||||
try:
|
||||
@ -49,7 +129,7 @@ class SendAdditionalEmailAction(BaseAction):
|
||||
for user in users:
|
||||
user_roles = [role.name for role in user.roles]
|
||||
if roles.intersection(user_roles):
|
||||
if settings.USERNAME_IS_EMAIL:
|
||||
if CONF.identity.username_is_email:
|
||||
self.emails.add(user.name)
|
||||
else:
|
||||
self.emails.add(user.email)
|
||||
@ -61,7 +141,7 @@ class SendAdditionalEmailAction(BaseAction):
|
||||
for email in task_emails:
|
||||
self.emails.add(email)
|
||||
|
||||
for email in conf.get('email_additional_addresses', []):
|
||||
for email in conf.get('email_additional_addresses'):
|
||||
self.emails.add(email)
|
||||
|
||||
def _validate(self):
|
||||
@ -69,13 +149,13 @@ class SendAdditionalEmailAction(BaseAction):
|
||||
self.action.save()
|
||||
|
||||
def _prepare(self):
|
||||
self.perform_action('initial')
|
||||
self.perform_action('prepare')
|
||||
|
||||
def _approve(self):
|
||||
self.perform_action('token')
|
||||
self.perform_action('approve')
|
||||
|
||||
def _submit(self, data):
|
||||
self.perform_action('completed')
|
||||
self.perform_action('submit')
|
||||
|
||||
def perform_action(self, stage):
|
||||
self._validate()
|
||||
@ -85,7 +165,7 @@ class SendAdditionalEmailAction(BaseAction):
|
||||
if not action.valid:
|
||||
return
|
||||
|
||||
email_conf = self.settings.get(stage, {})
|
||||
email_conf = self.config.get(stage)
|
||||
|
||||
# If either of these are false we won't be sending anything.
|
||||
if not email_conf or not email_conf.get('template'):
|
||||
|
@ -12,10 +12,9 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
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 (
|
||||
@ -29,9 +28,10 @@ from adjutant.actions.v1.resources import (
|
||||
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 settings dict with tuples in the format:
|
||||
# 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):
|
||||
@ -47,7 +47,14 @@ def register_action_class(action_class, serializer_class):
|
||||
)
|
||||
data = {}
|
||||
data[action_class.__name__] = (action_class, serializer_class)
|
||||
settings.ACTION_CLASSES.update(data)
|
||||
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:
|
||||
|
@ -14,9 +14,12 @@
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
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
|
||||
@ -38,6 +41,17 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
'description',
|
||||
]
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
"default_roles",
|
||||
help_text="Roles to be given on project to the creating user.",
|
||||
default=[],
|
||||
sample_default=["member", "project_admin"]
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NewProjectAction, self).__init__(*args, **kwargs)
|
||||
|
||||
@ -90,7 +104,7 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
self.action.task.cache['user_id'] = user_id
|
||||
self.add_note("User already given roles.")
|
||||
else:
|
||||
default_roles = self.settings.get("default_roles", {})
|
||||
default_roles = self.config.default_roles
|
||||
|
||||
project_id = self.get_cache('project_id')
|
||||
keystone_user = self.action.task.keystone_user
|
||||
@ -135,6 +149,17 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
'email'
|
||||
]
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
"default_roles",
|
||||
help_text="Roles to be given on project for the user.",
|
||||
default=[],
|
||||
sample_default=["member", "project_admin"]
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NewProjectWithUserAction, self).__init__(*args, **kwargs)
|
||||
|
||||
@ -166,7 +191,7 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
self.set_token_fields(["password"])
|
||||
return True
|
||||
|
||||
if (not settings.USERNAME_IS_EMAIL
|
||||
if (not CONF.identity.username_is_email
|
||||
and getattr(user, 'email', None) != self.email):
|
||||
self.add_note("Existing user '%s' with non-matching email." %
|
||||
self.username)
|
||||
@ -263,7 +288,7 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
|
||||
def _create_user_for_project(self):
|
||||
id_manager = user_store.IdentityManager()
|
||||
default_roles = self.settings.get("default_roles", {})
|
||||
default_roles = self.config.default_roles
|
||||
|
||||
project_id = self.get_cache('project_id')
|
||||
|
||||
@ -414,10 +439,25 @@ class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
'domain_id',
|
||||
]
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
"default_users",
|
||||
help_text="Users which this action should add to the project.",
|
||||
default=[],
|
||||
),
|
||||
fields.ListConfig(
|
||||
"default_roles",
|
||||
help_text="Roles which those users should get.",
|
||||
default=[],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AddDefaultUsersToProjectAction, self).__init__(*args, **kwargs)
|
||||
self.users = self.settings.get('default_users', [])
|
||||
self.roles = self.settings.get('default_roles', [])
|
||||
self.users = self.config.default_users
|
||||
self.roles = self.config.default_roles
|
||||
|
||||
def _validate_users(self):
|
||||
id_manager = user_store.IdentityManager()
|
||||
|
@ -12,16 +12,19 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from confspirator import groups
|
||||
from confspirator import fields
|
||||
|
||||
from adjutant.actions.v1.base import BaseAction, ProjectMixin, QuotaMixin
|
||||
from adjutant.actions.utils import validate_steps
|
||||
from adjutant.common import openstack_clients, user_store
|
||||
from adjutant.api import models
|
||||
from adjutant.common.quota import QuotaManager
|
||||
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
from datetime import timedelta
|
||||
from adjutant.config import CONF
|
||||
|
||||
|
||||
class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
||||
@ -37,6 +40,49 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
||||
'region',
|
||||
]
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
groups.ConfigGroup(
|
||||
"region_defaults",
|
||||
children=[
|
||||
fields.StrConfig(
|
||||
"network_name",
|
||||
help_text="Name to be given to the default network.",
|
||||
default="default_network",
|
||||
),
|
||||
fields.StrConfig(
|
||||
"subnet_name",
|
||||
help_text="Name to be given to the default subnet.",
|
||||
default="default_subnet",
|
||||
),
|
||||
fields.StrConfig(
|
||||
"router_name",
|
||||
help_text="Name to be given to the default router.",
|
||||
default="default_router",
|
||||
),
|
||||
fields.StrConfig(
|
||||
"public_network",
|
||||
help_text="ID of the public network.",
|
||||
),
|
||||
fields.StrConfig(
|
||||
"subnet_cidr",
|
||||
help_text="CIDR for the default subnet.",
|
||||
),
|
||||
fields.ListConfig(
|
||||
"dns_nameservers",
|
||||
help_text="DNS nameservers for the subnet.",
|
||||
),
|
||||
]
|
||||
),
|
||||
fields.DictConfig(
|
||||
"regions",
|
||||
help_text="Specific per region config for default network. "
|
||||
"See 'region_defaults'.",
|
||||
default={},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NewDefaultNetworkAction, self).__init__(*args, **kwargs)
|
||||
|
||||
@ -54,33 +100,28 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
||||
self.add_note('Region: %s exists.' % self.region)
|
||||
return True
|
||||
|
||||
def _validate_defaults(self):
|
||||
defaults = self.settings.get(self.region, {})
|
||||
|
||||
if not defaults:
|
||||
self.add_note('ERROR: No default settings for region %s.' %
|
||||
self.region)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _validate(self):
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_region,
|
||||
self._validate_project_id,
|
||||
self._validate_defaults,
|
||||
self._validate_keystone_user_project_id,
|
||||
])
|
||||
self.action.save()
|
||||
|
||||
def _create_network(self):
|
||||
neutron = openstack_clients.get_neutronclient(region=self.region)
|
||||
defaults = self.settings.get(self.region, {})
|
||||
try:
|
||||
region_config = self.config.regions[self.region]
|
||||
network_config = self.config.region_defaults.overlay(
|
||||
region_config)
|
||||
except KeyError:
|
||||
network_config = self.config.region_defaults
|
||||
|
||||
if not self.get_cache('network_id'):
|
||||
try:
|
||||
network_body = {
|
||||
"network": {
|
||||
"name": defaults['network_name'],
|
||||
"name": network_config.network_name,
|
||||
'tenant_id': self.project_id,
|
||||
"admin_state_up": True
|
||||
}
|
||||
@ -89,15 +130,15 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while creating network: %s" %
|
||||
(e, defaults['network_name']))
|
||||
(e, network_config.network_name))
|
||||
raise
|
||||
self.set_cache('network_id', network['network']['id'])
|
||||
self.add_note("Network %s created for project %s" %
|
||||
(defaults['network_name'],
|
||||
(network_config.network_name,
|
||||
self.project_id))
|
||||
else:
|
||||
self.add_note("Network %s already created for project %s" %
|
||||
(defaults['network_name'],
|
||||
(network_config.network_name,
|
||||
self.project_id))
|
||||
|
||||
if not self.get_cache('subnet_id'):
|
||||
@ -107,8 +148,8 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
||||
"network_id": self.get_cache('network_id'),
|
||||
"ip_version": 4,
|
||||
'tenant_id': self.project_id,
|
||||
'dns_nameservers': defaults['DNS_NAMESERVERS'],
|
||||
"cidr": defaults['SUBNET_CIDR']
|
||||
'dns_nameservers': network_config.dns_nameservers,
|
||||
"cidr": network_config.subnet_cidr
|
||||
}
|
||||
}
|
||||
subnet = neutron.create_subnet(body=subnet_body)
|
||||
@ -118,18 +159,18 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
||||
raise
|
||||
self.set_cache('subnet_id', subnet['subnet']['id'])
|
||||
self.add_note("Subnet created for network %s" %
|
||||
defaults['network_name'])
|
||||
network_config.network_name)
|
||||
else:
|
||||
self.add_note("Subnet already created for network %s" %
|
||||
defaults['network_name'])
|
||||
network_config.network_name)
|
||||
|
||||
if not self.get_cache('router_id'):
|
||||
try:
|
||||
router_body = {
|
||||
"router": {
|
||||
"name": defaults['router_name'],
|
||||
"name": network_config.router_name,
|
||||
"external_gateway_info": {
|
||||
"network_id": defaults['public_network']
|
||||
"network_id": network_config.public_network
|
||||
},
|
||||
'tenant_id': self.project_id,
|
||||
"admin_state_up": True
|
||||
@ -139,7 +180,7 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while creating router: %s" %
|
||||
(e, defaults['router_name']))
|
||||
(e, network_config.router_name))
|
||||
raise
|
||||
self.set_cache('router_id', router['router']['id'])
|
||||
self.add_note("Router created for project %s" %
|
||||
@ -195,7 +236,6 @@ class NewProjectDefaultNetworkAction(NewDefaultNetworkAction):
|
||||
# Note: Don't check project here as it doesn't exist yet.
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_region,
|
||||
self._validate_defaults,
|
||||
])
|
||||
self.action.save()
|
||||
|
||||
@ -203,7 +243,6 @@ class NewProjectDefaultNetworkAction(NewDefaultNetworkAction):
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_region,
|
||||
self._validate_project_id,
|
||||
self._validate_defaults,
|
||||
])
|
||||
self.action.save()
|
||||
|
||||
@ -227,17 +266,26 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
||||
'regions',
|
||||
]
|
||||
|
||||
default_days_between_autoapprove = 30
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UpdateProjectQuotasAction, self).__init__(*args, **kwargs)
|
||||
self.size_difference_threshold = settings.TASK_SETTINGS.get(
|
||||
self.action.task.task_type, {}).get(
|
||||
'size_difference_threshold')
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.FloatConfig(
|
||||
"size_difference_threshold",
|
||||
help_text="Precentage different allowed when matching quota sizes.",
|
||||
default=0.1,
|
||||
min=0,
|
||||
max=1,
|
||||
),
|
||||
fields.IntConfig(
|
||||
"days_between_autoapprove",
|
||||
help_text="The allowed number of days between auto approved quota changes.",
|
||||
default=30,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def _get_email(self):
|
||||
|
||||
if settings.USERNAME_IS_EMAIL:
|
||||
if CONF.identity.username_is_email:
|
||||
return self.action.task.keystone_user['username']
|
||||
else:
|
||||
id_manager = user_store.IdentityManager()
|
||||
@ -250,7 +298,7 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
||||
return None
|
||||
|
||||
def _validate_quota_size_exists(self):
|
||||
size_list = settings.PROJECT_QUOTA_SIZES.keys()
|
||||
size_list = CONF.quota.sizes.keys()
|
||||
if self.size not in size_list:
|
||||
self.add_note("Quota size: %s does not exist" % self.size)
|
||||
return False
|
||||
@ -258,24 +306,23 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
||||
|
||||
def _set_region_quota(self, region_name, quota_size):
|
||||
# Set the quota for an individual region
|
||||
quota_settings = settings.PROJECT_QUOTA_SIZES.get(quota_size, {})
|
||||
if not quota_settings:
|
||||
quota_config = CONF.quota.sizes.get(quota_size, {})
|
||||
if not quota_config:
|
||||
self.add_note(
|
||||
"Project quota not defined for size '%s' in region %s." % (
|
||||
quota_size, region_name))
|
||||
return
|
||||
|
||||
quota_manager = QuotaManager(self.project_id,
|
||||
self.size_difference_threshold)
|
||||
quota_manager = QuotaManager(
|
||||
self.project_id, self.config.size_difference_threshold)
|
||||
|
||||
quota_manager.set_region_quota(region_name, quota_settings)
|
||||
quota_manager.set_region_quota(region_name, quota_config)
|
||||
|
||||
self.add_note("Project quota for region %s set to %s" % (
|
||||
region_name, quota_size))
|
||||
|
||||
def _can_auto_approve(self):
|
||||
wait_days = self.settings.get('days_between_autoapprove',
|
||||
self.default_days_between_autoapprove)
|
||||
wait_days = self.config.days_between_autoapprove
|
||||
task_list = models.Task.objects.filter(
|
||||
completed_on__gte=timezone.now() - timedelta(days=wait_days),
|
||||
task_type__exact=self.action.task.task_type,
|
||||
@ -294,8 +341,8 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
||||
|
||||
region_sizes = []
|
||||
|
||||
quota_manager = QuotaManager(self.project_id,
|
||||
self.size_difference_threshold)
|
||||
quota_manager = QuotaManager(
|
||||
self.project_id, self.config.size_difference_threshold)
|
||||
|
||||
for region in self.regions:
|
||||
current_size = quota_manager.get_region_quota_data(
|
||||
@ -382,6 +429,17 @@ class SetProjectQuotaAction(UpdateProjectQuotasAction):
|
||||
""" Updates quota for a given project to a configured quota level """
|
||||
required = []
|
||||
|
||||
config_group = UpdateProjectQuotasAction.config_group.extend(
|
||||
children=[
|
||||
fields.DictConfig(
|
||||
"region_sizes",
|
||||
help_text="Which quota size to use for which region.",
|
||||
default={},
|
||||
sample_default={"RegionOne": "small"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def _get_email(self):
|
||||
return None
|
||||
|
||||
@ -406,10 +464,8 @@ class SetProjectQuotaAction(UpdateProjectQuotasAction):
|
||||
return
|
||||
|
||||
# update quota for each openstack service
|
||||
regions_dict = self.settings.get('regions', {})
|
||||
for region_name, region_settings in regions_dict.items():
|
||||
quota_size = region_settings.get('quota_size')
|
||||
self._set_region_quota(region_name, quota_size)
|
||||
for region_name, region_size in self.config.region_sizes.items():
|
||||
self._set_region_quota(region_name, region_size)
|
||||
|
||||
self.action.state = "completed"
|
||||
self.action.save()
|
||||
|
@ -13,12 +13,14 @@
|
||||
# under the License.
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.conf import settings
|
||||
|
||||
from adjutant.config import CONF
|
||||
from adjutant.common import user_store
|
||||
|
||||
|
||||
role_options = settings.DEFAULT_ACTION_SETTINGS.get("NewUserAction", {}).get(
|
||||
"allowed_roles", [])
|
||||
def get_role_choices():
|
||||
id_manager = user_store.IdentityManager()
|
||||
return id_manager.get_manageable_roles()
|
||||
|
||||
|
||||
def get_region_choices():
|
||||
@ -37,7 +39,7 @@ class BaseUserNameSerializer(serializers.Serializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BaseUserNameSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
if settings.USERNAME_IS_EMAIL:
|
||||
if CONF.identity.username_is_email:
|
||||
self.fields.pop('username')
|
||||
|
||||
|
||||
@ -46,12 +48,16 @@ class BaseUserIdSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class NewUserSerializer(BaseUserNameSerializer):
|
||||
roles = serializers.MultipleChoiceField(
|
||||
choices=role_options, default=set)
|
||||
inherited_roles = serializers.MultipleChoiceField(
|
||||
choices=role_options, default=set)
|
||||
project_id = serializers.CharField(max_length=64)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NewUserSerializer, self).__init__(*args, **kwargs)
|
||||
# NOTE(adriant): This overide is mostly in use so that it can be tested
|
||||
self.fields['roles'] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set)
|
||||
self.fields['inherited_roles'] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set)
|
||||
|
||||
def validate(self, data):
|
||||
if not data['roles'] and not data['inherited_roles']:
|
||||
raise serializers.ValidationError(
|
||||
@ -81,13 +87,17 @@ class ResetUserSerializer(BaseUserNameSerializer):
|
||||
|
||||
|
||||
class EditUserRolesSerializer(BaseUserIdSerializer):
|
||||
roles = serializers.MultipleChoiceField(
|
||||
choices=role_options, default=set)
|
||||
inherited_roles = serializers.MultipleChoiceField(
|
||||
choices=role_options, default=set)
|
||||
remove = serializers.BooleanField(default=False)
|
||||
project_id = serializers.CharField(max_length=64)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EditUserRolesSerializer, self).__init__(*args, **kwargs)
|
||||
# NOTE(adriant): This overide is mostly in use so that it can be tested
|
||||
self.fields['roles'] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set)
|
||||
self.fields['inherited_roles'] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set)
|
||||
|
||||
def validate(self, data):
|
||||
if not data['roles'] and not data['inherited_roles']:
|
||||
raise serializers.ValidationError(
|
||||
@ -139,7 +149,7 @@ class UpdateProjectQuotasSerializer(serializers.Serializer):
|
||||
"""
|
||||
Check that the size exists in the conf.
|
||||
"""
|
||||
size_list = settings.PROJECT_QUOTA_SIZES.keys()
|
||||
size_list = CONF.quota.sizes.keys()
|
||||
if value not in size_list:
|
||||
raise serializers.ValidationError("Quota size: %s is not valid"
|
||||
% value)
|
||||
|
@ -13,15 +13,18 @@
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
from smtplib import SMTPException
|
||||
|
||||
from django.core import mail
|
||||
|
||||
from confspirator.tests import utils as conf_utils
|
||||
|
||||
from adjutant.actions.v1.misc import SendAdditionalEmailAction
|
||||
from adjutant.actions.utils import send_email
|
||||
from adjutant.api.models import Task
|
||||
from adjutant.common.tests.fake_clients import FakeManager
|
||||
from adjutant.common.tests.utils import modify_dict_settings, AdjutantTestCase
|
||||
from smtplib import SMTPException
|
||||
from adjutant.common.tests.utils import AdjutantTestCase
|
||||
from adjutant.config import CONF
|
||||
|
||||
default_email_conf = {
|
||||
'from': "adjutant@example.com",
|
||||
@ -79,18 +82,17 @@ class MiscActionTests(AdjutantTestCase):
|
||||
|
||||
@mock.patch('adjutant.actions.utils.EmailMultiAlternatives',
|
||||
FailEmail)
|
||||
@modify_dict_settings(DEFAULT_ACTION_SETTINGS={
|
||||
'operation': 'update',
|
||||
'key_list': ['SendAdditionalEmailAction', 'token'],
|
||||
'value': {
|
||||
'email_task_cache': True,
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}
|
||||
}, DEFAULT_TASK_SETTINGS={
|
||||
'operation': 'delete',
|
||||
'key_list': ['notifications'],
|
||||
})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
|
||||
{'operation': 'overlay', 'value': {
|
||||
'email_task_cache': True,
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_send_additional_email_fail(self):
|
||||
"""
|
||||
Tests that a failure to send an additional email doesn't cause
|
||||
@ -102,7 +104,6 @@ class MiscActionTests(AdjutantTestCase):
|
||||
task_type='edit_roles',
|
||||
)
|
||||
|
||||
# setup settings
|
||||
action = SendAdditionalEmailAction({}, task=task, order=1)
|
||||
|
||||
action.prepare()
|
||||
@ -116,21 +117,23 @@ class MiscActionTests(AdjutantTestCase):
|
||||
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
self.assertTrue(
|
||||
"Unable to send additional email. Stage: token" in
|
||||
"Unable to send additional email. Stage: approve" in
|
||||
action.action.task.action_notes['SendAdditionalEmailAction'][1])
|
||||
|
||||
action.submit({})
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
@modify_dict_settings(DEFAULT_ACTION_SETTINGS={
|
||||
'operation': 'update',
|
||||
'key_list': ['SendAdditionalEmailAction', 'token'],
|
||||
'value': {
|
||||
'email_task_cache': True,
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}
|
||||
})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
|
||||
{'operation': 'overlay', 'value': {
|
||||
'email_task_cache': True,
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_send_additional_email_task_cache(self):
|
||||
"""
|
||||
Tests sending an additional email with the address placed in the
|
||||
@ -141,7 +144,6 @@ class MiscActionTests(AdjutantTestCase):
|
||||
keystone_user={}
|
||||
)
|
||||
|
||||
# setup settings
|
||||
action = SendAdditionalEmailAction({}, task=task, order=1)
|
||||
|
||||
action.prepare()
|
||||
@ -161,15 +163,17 @@ class MiscActionTests(AdjutantTestCase):
|
||||
self.assertEqual(action.valid, True)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
|
||||
@modify_dict_settings(DEFAULT_ACTION_SETTINGS={
|
||||
'operation': 'update',
|
||||
'key_list': ['SendAdditionalEmailAction', 'token'],
|
||||
'value': {
|
||||
'email_task_cache': True,
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}
|
||||
})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
|
||||
{'operation': 'overlay', 'value': {
|
||||
'email_task_cache': True,
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_send_additional_email_task_cache_none_set(self):
|
||||
"""
|
||||
Tests sending an additional email with 'email_task_cache' set but
|
||||
@ -180,7 +184,6 @@ class MiscActionTests(AdjutantTestCase):
|
||||
keystone_user={}
|
||||
)
|
||||
|
||||
# setup settings
|
||||
action = SendAdditionalEmailAction({}, task=task, order=1)
|
||||
|
||||
action.prepare()
|
||||
@ -194,16 +197,19 @@ class MiscActionTests(AdjutantTestCase):
|
||||
action.submit({})
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
@modify_dict_settings(DEFAULT_ACTION_SETTINGS={
|
||||
'operation': 'update',
|
||||
'key_list': ['SendAdditionalEmailAction', 'token'],
|
||||
'value': {
|
||||
'email_additional_addresses': ['anadminwhocares@example.com'],
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}
|
||||
})
|
||||
def test_send_additional_email_email_in_settings(self):
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
|
||||
{'operation': 'overlay', 'value': {
|
||||
'email_additional_addresses': [
|
||||
'anadminwhocares@example.com'],
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_send_additional_email_email_in_config(self):
|
||||
"""
|
||||
Tests sending an additional email with the address placed in the
|
||||
task cache.
|
||||
@ -213,7 +219,6 @@ class MiscActionTests(AdjutantTestCase):
|
||||
keystone_user={}
|
||||
)
|
||||
|
||||
# setup settings
|
||||
action = SendAdditionalEmailAction({}, task=task, order=1)
|
||||
|
||||
action.prepare()
|
||||
|
@ -12,11 +12,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import mock
|
||||
|
||||
from confspirator.tests import utils as conf_utils
|
||||
|
||||
from adjutant.actions.v1.projects import (
|
||||
NewProjectWithUserAction, AddDefaultUsersToProjectAction,
|
||||
NewProjectAction)
|
||||
@ -24,12 +23,31 @@ from adjutant.api.models import Task
|
||||
from adjutant.common.tests import fake_clients
|
||||
from adjutant.common.tests.fake_clients import (
|
||||
FakeManager, setup_identity_cache)
|
||||
from adjutant.common.tests.utils import modify_dict_settings
|
||||
from adjutant.common.tests.utils import AdjutantTestCase
|
||||
from adjutant.config import CONF
|
||||
|
||||
|
||||
@mock.patch('adjutant.common.user_store.IdentityManager',
|
||||
FakeManager)
|
||||
class ProjectActionTests(TestCase):
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.action_defaults.NewProjectWithUserAction.default_roles": [
|
||||
{'operation': 'override', 'value': [
|
||||
'member', 'heat_stack_owner', 'project_admin', 'project_mod']},
|
||||
],
|
||||
"adjutant.workflow.action_defaults.NewProjectAction.default_roles": [
|
||||
{'operation': 'override', 'value': [
|
||||
'member', 'heat_stack_owner', 'project_admin', 'project_mod']},
|
||||
],
|
||||
"adjutant.workflow.action_defaults.AddDefaultUsersToProjectAction.default_users": [
|
||||
{'operation': 'override', 'value': ['admin']},
|
||||
],
|
||||
"adjutant.workflow.action_defaults.AddDefaultUsersToProjectAction.default_roles": [
|
||||
{'operation': 'override', 'value': ['admin']},
|
||||
],
|
||||
})
|
||||
class ProjectActionTests(AdjutantTestCase):
|
||||
|
||||
def test_new_project(self):
|
||||
"""
|
||||
@ -82,7 +100,7 @@ class ProjectActionTests(TestCase):
|
||||
roles = fake_client._get_roles_as_names(new_user, new_project)
|
||||
self.assertEqual(
|
||||
sorted(roles),
|
||||
sorted(['_member_', 'project_admin',
|
||||
sorted(['member', 'project_admin',
|
||||
'project_mod', 'heat_stack_owner']))
|
||||
|
||||
def test_new_project_reapprove(self):
|
||||
@ -145,7 +163,7 @@ class ProjectActionTests(TestCase):
|
||||
roles = fake_client._get_roles_as_names(new_user, new_project)
|
||||
self.assertEqual(
|
||||
sorted(roles),
|
||||
sorted(['_member_', 'project_admin',
|
||||
sorted(['member', 'project_admin',
|
||||
'project_mod', 'heat_stack_owner']))
|
||||
|
||||
def test_new_project_reapprove_failure(self):
|
||||
@ -218,7 +236,7 @@ class ProjectActionTests(TestCase):
|
||||
roles = fake_client._get_roles_as_names(new_user, new_project)
|
||||
self.assertEqual(
|
||||
sorted(roles),
|
||||
sorted(['_member_', 'project_admin',
|
||||
sorted(['member', 'project_admin',
|
||||
'project_mod', 'heat_stack_owner']))
|
||||
|
||||
def test_new_project_existing_user(self):
|
||||
@ -271,10 +289,16 @@ class ProjectActionTests(TestCase):
|
||||
roles = fake_client._get_roles_as_names(user, new_project)
|
||||
self.assertEqual(
|
||||
sorted(roles),
|
||||
sorted(['_member_', 'project_admin',
|
||||
sorted(['member', 'project_admin',
|
||||
'project_mod', 'heat_stack_owner']))
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_new_project_user_nonmatching_email(self):
|
||||
"""
|
||||
Attempts to create a new project and a new user, where there is
|
||||
@ -447,7 +471,7 @@ class ProjectActionTests(TestCase):
|
||||
roles = fake_client._get_roles_as_names(user, new_project)
|
||||
self.assertEqual(
|
||||
sorted(roles),
|
||||
sorted(['_member_', 'project_admin',
|
||||
sorted(['member', 'project_admin',
|
||||
'project_mod', 'heat_stack_owner']))
|
||||
|
||||
def test_new_project_user_disabled_during_signup(self):
|
||||
@ -522,7 +546,7 @@ class ProjectActionTests(TestCase):
|
||||
roles = fake_client._get_roles_as_names(user, new_project)
|
||||
self.assertEqual(
|
||||
sorted(roles),
|
||||
sorted(['_member_', 'project_admin',
|
||||
sorted(['member', 'project_admin',
|
||||
'project_mod', 'heat_stack_owner']))
|
||||
|
||||
def test_new_project_existing_project(self):
|
||||
@ -583,7 +607,13 @@ class ProjectActionTests(TestCase):
|
||||
action.approve()
|
||||
self.assertEqual(action.valid, False)
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_new_project_email_not_username(self):
|
||||
"""
|
||||
Base case, no project, no user.
|
||||
@ -636,20 +666,14 @@ class ProjectActionTests(TestCase):
|
||||
roles = fake_client._get_roles_as_names(new_user, new_project)
|
||||
self.assertEqual(
|
||||
sorted(roles),
|
||||
sorted(['_member_', 'project_admin',
|
||||
sorted(['member', 'project_admin',
|
||||
'project_mod', 'heat_stack_owner']))
|
||||
|
||||
@modify_dict_settings(DEFAULT_ACTION_SETTINGS={
|
||||
'key_list': ['AddDefaultUsersToProjectAction'],
|
||||
'operation': 'override',
|
||||
'value': {'default_users': ['admin', ],
|
||||
'default_roles': ['admin', ]}})
|
||||
def test_add_default_users(self):
|
||||
"""
|
||||
Base case, adds admin user with admin role to project.
|
||||
|
||||
NOTE(adriant): both the lists of users, and the roles to add
|
||||
come from test_settings. This test assumes the conf setting of:
|
||||
NOTE(adriant): This test assumes the conf setting of:
|
||||
default_users = ['admin']
|
||||
default_roles = ['admin']
|
||||
"""
|
||||
@ -699,11 +723,6 @@ class ProjectActionTests(TestCase):
|
||||
# Now the missing project should make the action invalid
|
||||
self.assertEqual(action.valid, False)
|
||||
|
||||
@modify_dict_settings(DEFAULT_ACTION_SETTINGS={
|
||||
'key_list': ['AddDefaultUsersToProjectAction'],
|
||||
'operation': 'override',
|
||||
'value': {'default_users': ['admin', ],
|
||||
'default_roles': ['admin', ]}})
|
||||
def test_add_default_users_reapprove(self):
|
||||
"""
|
||||
Ensure nothing happens or changes during rerun of approve.
|
||||
@ -777,7 +796,7 @@ class ProjectActionTests(TestCase):
|
||||
roles = fake_client._get_roles_as_names(user, new_project)
|
||||
self.assertEqual(
|
||||
sorted(roles),
|
||||
sorted(['_member_', 'project_admin',
|
||||
sorted(['member', 'project_admin',
|
||||
'project_mod', 'heat_stack_owner']))
|
||||
|
||||
action.submit({})
|
||||
@ -824,7 +843,7 @@ class ProjectActionTests(TestCase):
|
||||
roles = fake_client._get_roles_as_names(user, new_project)
|
||||
self.assertEqual(
|
||||
sorted(roles),
|
||||
sorted(['_member_', 'project_admin',
|
||||
sorted(['member', 'project_admin',
|
||||
'project_mod', 'heat_stack_owner']))
|
||||
|
||||
action.approve()
|
||||
@ -837,7 +856,7 @@ class ProjectActionTests(TestCase):
|
||||
roles = fake_client._get_roles_as_names(user, new_project)
|
||||
self.assertEqual(
|
||||
sorted(roles),
|
||||
sorted(['_member_', 'project_admin',
|
||||
sorted(['member', 'project_admin',
|
||||
'project_mod', 'heat_stack_owner']))
|
||||
|
||||
action.submit({})
|
||||
|
@ -12,20 +12,20 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import mock
|
||||
|
||||
from confspirator.tests import utils as conf_utils
|
||||
|
||||
from adjutant.actions.v1.resources import (
|
||||
NewDefaultNetworkAction, NewProjectDefaultNetworkAction,
|
||||
SetProjectQuotaAction, UpdateProjectQuotasAction)
|
||||
from adjutant.api.models import Task
|
||||
from adjutant.common.tests.utils import modify_dict_settings
|
||||
from adjutant.common.tests.fake_clients import (
|
||||
FakeManager, setup_identity_cache, get_fake_neutron, get_fake_novaclient,
|
||||
get_fake_cinderclient, setup_neutron_cache, neutron_cache, cinder_cache,
|
||||
nova_cache, setup_mock_caches, get_fake_octaviaclient, octavia_cache)
|
||||
from adjutant.common.tests.utils import AdjutantTestCase
|
||||
from adjutant.config import CONF
|
||||
|
||||
|
||||
@mock.patch('adjutant.common.user_store.IdentityManager',
|
||||
@ -42,7 +42,43 @@ from adjutant.common.tests.fake_clients import (
|
||||
@mock.patch(
|
||||
'adjutant.common.openstack_clients.get_cinderclient',
|
||||
get_fake_cinderclient)
|
||||
class ProjectSetupActionTests(TestCase):
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.action_defaults.NewDefaultNetworkAction.regions": [
|
||||
{
|
||||
"operation": "override",
|
||||
"value": {
|
||||
"RegionOne": {
|
||||
"dns_nameservers": ["193.168.1.2", "193.168.1.3"],
|
||||
"subnet_cidr": "192.168.1.0/24",
|
||||
"network_name": "somenetwork",
|
||||
"public_network": "3cb50f61-5bce-4c03-96e6-8e262e12bb35",
|
||||
"router_name": "somerouter",
|
||||
"subnet_name": "somesubnet",
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
"adjutant.quota.sizes": [
|
||||
{
|
||||
"operation": "update",
|
||||
"value": {
|
||||
"large_cinder_only": {
|
||||
"cinder": {"gigabytes": 50001, "volumes": 200, "snapshots": 600}
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
"adjutant.workflow.action_defaults.SetProjectQuotaAction.region_sizes": [
|
||||
{
|
||||
"operation": "override",
|
||||
"value": {'RegionOne': 'small', 'RegionThree': 'large_cinder_only'}
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
class ProjectSetupActionTests(AdjutantTestCase):
|
||||
|
||||
def test_network_setup(self):
|
||||
"""
|
||||
@ -207,21 +243,11 @@ class ProjectSetupActionTests(TestCase):
|
||||
self.assertEqual(len(
|
||||
neutron_cache['RegionOne']['test_project_id']['subnets']), 1)
|
||||
|
||||
@modify_dict_settings(DEFAULT_ACTION_SETTINGS={
|
||||
'operation': 'override',
|
||||
'key_list': ['NewDefaultNetworkAction'],
|
||||
'value': {'RegionOne': {
|
||||
'DNS_NAMESERVERS': ['193.168.1.2', '193.168.1.3'],
|
||||
'SUBNET_CIDR': '192.168.1.0/24',
|
||||
'network_name': 'somenetwork',
|
||||
'public_network': '3cb50f61-5bce-4c03-96e6-8e262e12bb35',
|
||||
'router_name': 'somerouter',
|
||||
'subnet_name': 'somesubnet'
|
||||
}}})
|
||||
def test_new_project_network_setup(self):
|
||||
"""
|
||||
Base case, setup network after a new project, no issues.
|
||||
"""
|
||||
setup_identity_cache()
|
||||
setup_neutron_cache('RegionOne', 'test_project_id')
|
||||
task = Task.objects.create(
|
||||
keystone_user={'roles': ['admin']})
|
||||
@ -272,6 +298,7 @@ class ProjectSetupActionTests(TestCase):
|
||||
"""
|
||||
No project id given, should do nothing.
|
||||
"""
|
||||
setup_identity_cache()
|
||||
setup_neutron_cache('RegionOne', 'test_project_id')
|
||||
task = Task.objects.create(
|
||||
keystone_user={'roles': ['admin']})
|
||||
@ -304,6 +331,7 @@ class ProjectSetupActionTests(TestCase):
|
||||
"""
|
||||
Told not to setup, should do nothing.
|
||||
"""
|
||||
setup_identity_cache()
|
||||
setup_neutron_cache('RegionOne', 'test_project_id')
|
||||
task = Task.objects.create(
|
||||
keystone_user={'roles': ['admin']})
|
||||
@ -348,6 +376,7 @@ class ProjectSetupActionTests(TestCase):
|
||||
"""
|
||||
Should fail, but on re_approve will continue where it left off.
|
||||
"""
|
||||
setup_identity_cache()
|
||||
setup_neutron_cache('RegionOne', 'test_project_id')
|
||||
global neutron_cache
|
||||
task = Task.objects.create(
|
||||
@ -443,7 +472,6 @@ class ProjectSetupActionTests(TestCase):
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
# check the quotas were updated
|
||||
# This relies on test_settings heavily.
|
||||
cinderquota = cinder_cache['RegionOne']['test_project_id']['quota']
|
||||
self.assertEqual(cinderquota['gigabytes'], 5000)
|
||||
novaquota = nova_cache['RegionOne']['test_project_id']['quota']
|
||||
@ -453,8 +481,7 @@ class ProjectSetupActionTests(TestCase):
|
||||
|
||||
# RegionThree, cinder only
|
||||
self.assertFalse('RegionThree' in nova_cache)
|
||||
r2_cinderquota = cinder_cache['RegionThree'][
|
||||
'test_project_id']['quota']
|
||||
r2_cinderquota = cinder_cache['RegionThree']['test_project_id']['quota']
|
||||
self.assertEqual(r2_cinderquota['gigabytes'], 50001)
|
||||
self.assertEqual(r2_cinderquota['snapshots'], 600)
|
||||
self.assertEqual(r2_cinderquota['volumes'], 200)
|
||||
@ -475,7 +502,7 @@ class ProjectSetupActionTests(TestCase):
|
||||
@mock.patch(
|
||||
'adjutant.common.openstack_clients.get_octaviaclient',
|
||||
get_fake_octaviaclient)
|
||||
class QuotaActionTests(TestCase):
|
||||
class QuotaActionTests(AdjutantTestCase):
|
||||
|
||||
def test_update_quota(self):
|
||||
"""
|
||||
@ -495,7 +522,7 @@ class QuotaActionTests(TestCase):
|
||||
user.password = "test_password"
|
||||
|
||||
setup_identity_cache(projects=[project], users=[user])
|
||||
setup_neutron_cache('RegionOne', 'test_project_id')
|
||||
setup_mock_caches('RegionOne', 'test_project_id')
|
||||
|
||||
# Test sending to only a single region
|
||||
task = Task.objects.create(
|
||||
@ -517,7 +544,6 @@ class QuotaActionTests(TestCase):
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
# check the quotas were updated
|
||||
# This relies on test_settings heavily.
|
||||
cinderquota = cinder_cache['RegionOne']['test_project_id']['quota']
|
||||
self.assertEqual(cinderquota['gigabytes'], 10000)
|
||||
novaquota = nova_cache['RegionOne']['test_project_id']['quota']
|
||||
@ -566,7 +592,6 @@ class QuotaActionTests(TestCase):
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
# check the quotas were updated
|
||||
# This relies on test_settings heavily.
|
||||
cinderquota = cinder_cache['RegionOne']['test_project_id']['quota']
|
||||
self.assertEqual(cinderquota['gigabytes'], 50000)
|
||||
novaquota = nova_cache['RegionOne']['test_project_id']['quota']
|
||||
@ -581,7 +606,13 @@ class QuotaActionTests(TestCase):
|
||||
neutronquota = neutron_cache['RegionTwo']['test_project_id']['quota']
|
||||
self.assertEqual(neutronquota['network'], 10)
|
||||
|
||||
@override_settings(QUOTA_SIZES_ASC=[])
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.quota.sizes_ascending": [
|
||||
{'operation': 'override', 'value': []},
|
||||
],
|
||||
})
|
||||
def test_update_quota_not_in_sizes_asc(self):
|
||||
"""
|
||||
Tests that the quota will still update to a size even if it is not
|
||||
@ -624,7 +655,6 @@ class QuotaActionTests(TestCase):
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
# check the quotas were updated
|
||||
# This relies on test_settings heavily.
|
||||
cinderquota = cinder_cache['RegionOne']['test_project_id']['quota']
|
||||
self.assertEqual(cinderquota['gigabytes'], 50000)
|
||||
novaquota = nova_cache['RegionOne']['test_project_id']['quota']
|
||||
@ -639,11 +669,17 @@ class QuotaActionTests(TestCase):
|
||||
neutronquota = neutron_cache['RegionTwo']['test_project_id']['quota']
|
||||
self.assertEqual(neutronquota['network'], 10)
|
||||
|
||||
@modify_dict_settings(QUOTA_SERVICES={
|
||||
'operation': 'append',
|
||||
'key_list': ['*'],
|
||||
'value': 'octavia'
|
||||
})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.quota.services": [
|
||||
{
|
||||
"operation": "override",
|
||||
"value": {"*": ["cinder", "neutron", "nova", "octavia"]},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
def test_update_quota_octavia(self):
|
||||
"""Tests the quota update of the octavia service"""
|
||||
project = mock.Mock()
|
||||
@ -681,7 +717,6 @@ class QuotaActionTests(TestCase):
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
# check the quotas were updated
|
||||
# This relies on test_settings heavily.
|
||||
cinderquota = cinder_cache['RegionOne']['test_project_id']['quota']
|
||||
self.assertEqual(cinderquota['gigabytes'], 50000)
|
||||
novaquota = nova_cache['RegionOne']['test_project_id']['quota']
|
||||
@ -691,11 +726,17 @@ class QuotaActionTests(TestCase):
|
||||
octaviaquota = octavia_cache['RegionOne']['test_project_id']['quota']
|
||||
self.assertEqual(octaviaquota['load_balancer'], 10)
|
||||
|
||||
@modify_dict_settings(QUOTA_SERVICES={
|
||||
'operation': 'append',
|
||||
'key_list': ['*'],
|
||||
'value': 'octavia'
|
||||
})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.quota.services": [
|
||||
{
|
||||
"operation": "override",
|
||||
"value": {"*": ["cinder", "neutron", "nova", "octavia"]},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
def test_update_quota_octavia_over_usage(self):
|
||||
"""When octavia usage is higher than new quota it won't be changed"""
|
||||
project = mock.Mock()
|
||||
@ -738,7 +779,6 @@ class QuotaActionTests(TestCase):
|
||||
self.assertEqual(action.valid, False)
|
||||
|
||||
# check the quotas were updated
|
||||
# This relies on test_settings heavily.
|
||||
octaviaquota = octavia_cache['RegionOne']['test_project_id']['quota']
|
||||
# Still set to default
|
||||
self.assertEqual(octaviaquota['load_balancer'], 1)
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import mock
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from confspirator.tests import utils as conf_utils
|
||||
|
||||
from adjutant.actions.v1.users import (
|
||||
EditUserRolesAction, NewUserAction, ResetUserPasswordAction,
|
||||
@ -22,11 +22,29 @@ from adjutant.actions.v1.users import (
|
||||
from adjutant.api.models import Task
|
||||
from adjutant.common.tests import fake_clients
|
||||
from adjutant.common.tests.fake_clients import setup_identity_cache
|
||||
from adjutant.common.tests.utils import modify_dict_settings, AdjutantTestCase
|
||||
from adjutant.common.tests.utils import AdjutantTestCase
|
||||
from adjutant.config import CONF
|
||||
|
||||
|
||||
@mock.patch('adjutant.common.user_store.IdentityManager',
|
||||
fake_clients.FakeManager)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.role_mapping": [
|
||||
{'operation': 'override', 'value': {
|
||||
'admin': [
|
||||
'project_admin', 'project_mod', 'member', 'heat_stack_owner'
|
||||
],
|
||||
'project_admin': [
|
||||
'project_mod', 'member', 'heat_stack_owner', 'project_admin',
|
||||
],
|
||||
'project_mod': [
|
||||
'member', 'heat_stack_owner', 'project_mod',
|
||||
],
|
||||
}},
|
||||
],
|
||||
})
|
||||
class UserActionTests(AdjutantTestCase):
|
||||
|
||||
def test_new_user(self):
|
||||
@ -48,7 +66,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
data = {
|
||||
'email': 'test@example.com',
|
||||
'project_id': project.id,
|
||||
'roles': ['_member_'],
|
||||
'roles': ['member'],
|
||||
'inherited_roles': [],
|
||||
'domain_id': 'default',
|
||||
}
|
||||
@ -75,7 +93,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
self.assertEqual(user.password, '123456')
|
||||
|
||||
roles = fake_client._get_roles_as_names(user, project)
|
||||
self.assertEqual(roles, ['_member_'])
|
||||
self.assertEqual(roles, ['member'])
|
||||
|
||||
def test_new_user_existing(self):
|
||||
"""
|
||||
@ -98,7 +116,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
data = {
|
||||
'email': 'test@example.com',
|
||||
'project_id': project.id,
|
||||
'roles': ['_member_'],
|
||||
'roles': ['member'],
|
||||
'inherited_roles': [],
|
||||
'domain_id': 'default',
|
||||
}
|
||||
@ -118,7 +136,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
fake_client = fake_clients.FakeManager()
|
||||
|
||||
roles = fake_client._get_roles_as_names(user, project)
|
||||
self.assertEqual(roles, ['_member_'])
|
||||
self.assertEqual(roles, ['member'])
|
||||
|
||||
def test_new_user_disabled(self):
|
||||
"""
|
||||
@ -143,7 +161,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
data = {
|
||||
'email': 'test@example.com',
|
||||
'project_id': project.id,
|
||||
'roles': ['_member_'],
|
||||
'roles': ['member'],
|
||||
'inherited_roles': [],
|
||||
'domain_id': 'default',
|
||||
}
|
||||
@ -170,7 +188,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
self.assertTrue(user.enabled)
|
||||
|
||||
roles = fake_client._get_roles_as_names(user, project)
|
||||
self.assertEqual(roles, ['_member_'])
|
||||
self.assertEqual(roles, ['member'])
|
||||
|
||||
def test_new_user_existing_role(self):
|
||||
"""
|
||||
@ -187,7 +205,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
|
||||
assignment = fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
)
|
||||
|
||||
@ -204,7 +222,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
data = {
|
||||
'email': 'test@example.com',
|
||||
'project_id': project.id,
|
||||
'roles': ['_member_'],
|
||||
'roles': ['member'],
|
||||
'inherited_roles': [],
|
||||
'domain_id': 'default',
|
||||
}
|
||||
@ -225,7 +243,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
fake_client = fake_clients.FakeManager()
|
||||
|
||||
roles = fake_client._get_roles_as_names(user, project)
|
||||
self.assertEqual(roles, ['_member_'])
|
||||
self.assertEqual(roles, ['member'])
|
||||
|
||||
def test_new_user_no_tenant(self):
|
||||
"""
|
||||
@ -244,7 +262,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
data = {
|
||||
'email': 'test@example.com',
|
||||
'project_id': 'test_project_id',
|
||||
'roles': ['_member_'],
|
||||
'roles': ['member'],
|
||||
'inherited_roles': [],
|
||||
'domain_id': 'default',
|
||||
}
|
||||
@ -285,7 +303,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
data = {
|
||||
'email': 'test@example.com',
|
||||
'project_id': 'test_project_id_1',
|
||||
'roles': ['_member_'],
|
||||
'roles': ['member'],
|
||||
'inherited_roles': [],
|
||||
'domain_id': 'default',
|
||||
}
|
||||
@ -311,7 +329,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
|
||||
task = Task.objects.create(
|
||||
keystone_user={
|
||||
'roles': ['_member_'],
|
||||
'roles': ['member'],
|
||||
'project_id': project.id,
|
||||
'project_domain_id': 'default',
|
||||
})
|
||||
@ -319,7 +337,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
data = {
|
||||
'email': 'test@example.com',
|
||||
'project_id': project.id,
|
||||
'roles': ['_member_'],
|
||||
'roles': ['member'],
|
||||
'inherited_roles': [],
|
||||
'domain_id': 'default',
|
||||
}
|
||||
@ -343,7 +361,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
|
||||
assignment = fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
)
|
||||
|
||||
@ -360,7 +378,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
data = {
|
||||
'email': 'test@example.com',
|
||||
'project_id': project.id,
|
||||
'roles': ['_member_'],
|
||||
'roles': ['member'],
|
||||
'inherited_roles': [],
|
||||
'domain_id': 'not_default',
|
||||
}
|
||||
@ -504,7 +522,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
'domain_id': 'default',
|
||||
'user_id': user.id,
|
||||
'project_id': project.id,
|
||||
'roles': ['_member_', 'project_mod'],
|
||||
'roles': ['member', 'project_mod'],
|
||||
'inherited_roles': [],
|
||||
'remove': False
|
||||
}
|
||||
@ -524,7 +542,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
fake_client = fake_clients.FakeManager()
|
||||
|
||||
roles = fake_client._get_roles_as_names(user, project)
|
||||
self.assertEqual(sorted(roles), sorted(['_member_', 'project_mod']))
|
||||
self.assertEqual(sorted(roles), sorted(['member', 'project_mod']))
|
||||
|
||||
def test_edit_user_roles_add_complete(self):
|
||||
"""
|
||||
@ -538,7 +556,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
assignments = [
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
@ -562,7 +580,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
'domain_id': 'default',
|
||||
'user_id': user.id,
|
||||
'project_id': project.id,
|
||||
'roles': ['_member_', 'project_mod'],
|
||||
'roles': ['member', 'project_mod'],
|
||||
'inherited_roles': [],
|
||||
'remove': False
|
||||
}
|
||||
@ -583,7 +601,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
fake_client = fake_clients.FakeManager()
|
||||
|
||||
roles = fake_client._get_roles_as_names(user, project)
|
||||
self.assertEqual(roles, ['_member_', 'project_mod'])
|
||||
self.assertEqual(roles, ['member', 'project_mod'])
|
||||
|
||||
def test_edit_user_roles_remove(self):
|
||||
"""
|
||||
@ -598,7 +616,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
assignments = [
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
@ -642,7 +660,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
fake_client = fake_clients.FakeManager()
|
||||
|
||||
roles = fake_client._get_roles_as_names(user, project)
|
||||
self.assertEqual(roles, ['_member_'])
|
||||
self.assertEqual(roles, ['member'])
|
||||
|
||||
def test_edit_user_roles_remove_complete(self):
|
||||
"""
|
||||
@ -656,7 +674,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
|
||||
assignment = fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
)
|
||||
|
||||
@ -695,7 +713,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
fake_client = fake_clients.FakeManager()
|
||||
|
||||
roles = fake_client._get_roles_as_names(user, project)
|
||||
self.assertEqual(roles, ['_member_'])
|
||||
self.assertEqual(roles, ['member'])
|
||||
|
||||
def test_edit_user_roles_can_manage_all(self):
|
||||
"""
|
||||
@ -710,7 +728,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
assignments = [
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
@ -747,11 +765,11 @@ class UserActionTests(AdjutantTestCase):
|
||||
fake_client = fake_clients.FakeManager()
|
||||
|
||||
roles = fake_client._get_roles_as_names(user, project)
|
||||
self.assertEqual(roles, ['_member_', 'project_admin'])
|
||||
self.assertEqual(roles, ['member', 'project_admin'])
|
||||
|
||||
def test_edit_user_roles_modified_settings(self):
|
||||
def test_edit_user_roles_modified_config(self):
|
||||
"""
|
||||
Tests that the role mappings do come from settings and that they
|
||||
Tests that the role mappings do come from config and that they
|
||||
are enforced.
|
||||
"""
|
||||
project = fake_clients.FakeProject(name="test_project")
|
||||
@ -789,11 +807,18 @@ class UserActionTests(AdjutantTestCase):
|
||||
action.prepare()
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
# Change settings
|
||||
with self.modify_dict_settings(ROLES_MAPPING={
|
||||
'key_list': ['project_mod'],
|
||||
'operation': "remove",
|
||||
'value': 'heat_stack_owner'}):
|
||||
# Change config
|
||||
with conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.role_mapping": [
|
||||
{'operation': 'update', 'value': {
|
||||
'project_mod': [
|
||||
'member', 'project_mod',
|
||||
],
|
||||
}},
|
||||
],
|
||||
}):
|
||||
action.approve()
|
||||
self.assertEqual(action.valid, False)
|
||||
|
||||
@ -814,11 +839,20 @@ class UserActionTests(AdjutantTestCase):
|
||||
roles = fake_client._get_roles_as_names(user, project)
|
||||
self.assertEqual(roles, ['project_mod', 'heat_stack_owner'])
|
||||
|
||||
@modify_dict_settings(ROLES_MAPPING={'key_list': ['project_mod'],
|
||||
'operation': "append", 'value': 'new_role'})
|
||||
def test_edit_user_roles_modified_settings_add(self):
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.role_mapping": [
|
||||
{'operation': 'update', 'value': {
|
||||
'project_mod': [
|
||||
'member', 'heat_stack_owner', 'project_mod', 'new_role',
|
||||
],
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_edit_user_roles_modified_config_add(self):
|
||||
"""
|
||||
Tests that the role mappings do come from settings and a new role
|
||||
Tests that the role mappings do come from config and a new role
|
||||
added there will be allowed.
|
||||
"""
|
||||
project = fake_clients.FakeProject(name="test_project")
|
||||
@ -873,7 +907,13 @@ class UserActionTests(AdjutantTestCase):
|
||||
self.assertEqual(roles, ['project_mod', 'new_role'])
|
||||
|
||||
# Simple positive tests for when USERNAME_IS_EMAIL=False
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_create_user_email_not_username(self):
|
||||
"""
|
||||
Test the default case, all valid.
|
||||
@ -895,7 +935,7 @@ class UserActionTests(AdjutantTestCase):
|
||||
'username': 'test_user',
|
||||
'email': 'test@example.com',
|
||||
'project_id': project.id,
|
||||
'roles': ['_member_'],
|
||||
'roles': ['member'],
|
||||
'inherited_roles': [],
|
||||
'domain_id': 'default',
|
||||
}
|
||||
@ -922,9 +962,15 @@ class UserActionTests(AdjutantTestCase):
|
||||
self.assertTrue(user.enabled)
|
||||
|
||||
roles = fake_client._get_roles_as_names(user, project)
|
||||
self.assertEqual(roles, ['_member_'])
|
||||
self.assertEqual(roles, ['member'])
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_reset_user_email_not_username(self):
|
||||
"""
|
||||
Base case, existing user.
|
||||
@ -968,7 +1014,13 @@ class UserActionTests(AdjutantTestCase):
|
||||
self.assertEqual(user.email, 'test@example.com')
|
||||
self.assertEqual(user.password, '123456')
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_reset_user_password_case_insensitive_not_username(self):
|
||||
"""
|
||||
Existing user, ensure action is case insensitive.
|
||||
@ -1010,7 +1062,6 @@ class UserActionTests(AdjutantTestCase):
|
||||
fake_clients.identity_cache['users'][user.id].password,
|
||||
'123456')
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=True)
|
||||
def test_update_email(self):
|
||||
"""
|
||||
Base test case for user updating email address.
|
||||
@ -1054,7 +1105,6 @@ class UserActionTests(AdjutantTestCase):
|
||||
fake_clients.identity_cache['users'][user.id].name,
|
||||
'new_test@example.com')
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=True)
|
||||
def test_update_email_invalid_user(self):
|
||||
"""
|
||||
Test case for an invalid user being updated.
|
||||
@ -1086,7 +1136,13 @@ class UserActionTests(AdjutantTestCase):
|
||||
action.submit(token_data)
|
||||
self.assertEqual(action.valid, False)
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_update_email_username_not_email(self):
|
||||
"""
|
||||
Test case for a user attempting to update with an invalid email.
|
||||
|
@ -12,9 +12,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from confspirator import groups
|
||||
from confspirator import fields
|
||||
|
||||
from adjutant.config import CONF
|
||||
from adjutant.common import user_store
|
||||
from adjutant.actions.v1.base import (
|
||||
UserNameAction, UserIdAction, UserMixin, ProjectMixin)
|
||||
@ -58,7 +59,7 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
self.action.task.cache['user_state'] = "default"
|
||||
self.set_token_fields(["password"])
|
||||
return True
|
||||
if (not settings.USERNAME_IS_EMAIL
|
||||
if (not CONF.identity.username_is_email
|
||||
and getattr(user, 'email', None) != self.email):
|
||||
self.add_note(
|
||||
'Found matching username, but email did not match. '
|
||||
@ -174,18 +175,25 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
|
||||
Simple action to reset a password for a given user.
|
||||
"""
|
||||
|
||||
username = models.CharField(max_length=200)
|
||||
email = models.EmailField()
|
||||
|
||||
required = [
|
||||
'domain_name',
|
||||
'username',
|
||||
'email'
|
||||
]
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
"blacklisted_roles",
|
||||
help_text="Users with these roles cannot reset their passwords.",
|
||||
default=[],
|
||||
sample_default=['admin'],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ResetUserPasswordAction, self).__init__(*args, **kwargs)
|
||||
self.blacklist = self.settings.get("blacklisted_roles", [])
|
||||
|
||||
def _validate_user_roles(self):
|
||||
id_manager = user_store.IdentityManager()
|
||||
@ -196,7 +204,7 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
|
||||
for roles in roles.values():
|
||||
user_roles.extend(role.name for role in roles)
|
||||
|
||||
if set(self.blacklist) & set(user_roles):
|
||||
if set(self.config.blacklisted_roles) & set(user_roles):
|
||||
self.add_note('Cannot reset users with blacklisted roles.')
|
||||
return False
|
||||
|
||||
@ -205,7 +213,7 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
|
||||
def _validate_user_email(self):
|
||||
# NOTE(adriant): We only need to check the USERNAME_IS_EMAIL=False
|
||||
# case since '_validate_username_exists' will ensure the True case
|
||||
if not settings.USERNAME_IS_EMAIL:
|
||||
if not CONF.identity.username_is_email:
|
||||
if (self.user and (
|
||||
getattr(self.user, 'email', None).lower()
|
||||
!= self.email.lower())):
|
||||
@ -316,13 +324,13 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
|
||||
user=self.user_id)
|
||||
current_user_roles = [role.name for role in current_user_roles]
|
||||
|
||||
current_roles_manageable = self.are_roles_managable(
|
||||
current_roles_manageable = self.are_roles_manageable(
|
||||
self.action.task.keystone_user['roles'], current_user_roles)
|
||||
|
||||
all_roles = set()
|
||||
all_roles.update(self.roles)
|
||||
all_roles.update(self.inherited_roles)
|
||||
new_roles_manageable = self.are_roles_managable(
|
||||
new_roles_manageable = self.are_roles_manageable(
|
||||
self.action.task.keystone_user['roles'], all_roles)
|
||||
|
||||
return new_roles_manageable and current_roles_manageable
|
||||
@ -414,7 +422,7 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
|
||||
return False
|
||||
|
||||
def _validate_email_not_in_use(self):
|
||||
if settings.USERNAME_IS_EMAIL:
|
||||
if CONF.identity.username_is_email:
|
||||
self.domain_id = self.action.task.keystone_user[
|
||||
'project_domain_id']
|
||||
|
||||
@ -445,7 +453,7 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
|
||||
self.old_username = str(self.user.name)
|
||||
self.update_email(self.new_email, user=self.user)
|
||||
|
||||
if settings.USERNAME_IS_EMAIL:
|
||||
if CONF.identity.username_is_email:
|
||||
self.update_user_name(self.new_email, user=self.user)
|
||||
|
||||
self.add_note('The email for user %s has been changed to %s.'
|
||||
|
@ -0,0 +1,17 @@
|
||||
# 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.
|
||||
|
||||
# Dict of DelegateAPIs and their url_paths.
|
||||
# - This is populated by registering DelegateAPIs.
|
||||
DELEGATE_API_CLASSES = {}
|
@ -20,7 +20,7 @@ from django.utils import timezone
|
||||
from rest_framework.response import Response
|
||||
|
||||
from adjutant import exceptions
|
||||
from adjutant.api.v1.utils import create_notification
|
||||
from adjutant.notifications.utils import create_notification
|
||||
|
||||
|
||||
LOG = getLogger('adjutant')
|
||||
|
@ -14,9 +14,20 @@
|
||||
|
||||
from adjutant.api.v1.views import APIViewWithLogger
|
||||
|
||||
from adjutant.config import CONF
|
||||
|
||||
|
||||
# TODO(adriant): Decide what this class does now other than just being a
|
||||
# namespace for plugin views.
|
||||
class BaseDelegateAPI(APIViewWithLogger):
|
||||
"""Base Class for Adjutant's deployer configurable APIs."""
|
||||
pass
|
||||
|
||||
config_group = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BaseDelegateAPI, self).__init__(*args, **kwargs)
|
||||
# NOTE(adriant): This is only used at registration,
|
||||
# so lets not expose it:
|
||||
self.config_group = None
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return CONF.api.delegate_apis.get(self.__class__.__name__)
|
||||
|
@ -12,25 +12,32 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
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):
|
||||
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__
|
||||
% api_class.__name__
|
||||
)
|
||||
data = {}
|
||||
data[API_class.__name__] = {
|
||||
'class': API_class,
|
||||
data[api_class.__name__] = {
|
||||
'class': api_class,
|
||||
'url': url}
|
||||
settings.DELEGATE_API_CLASSES.update(data)
|
||||
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(
|
||||
|
@ -12,34 +12,47 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
from rest_framework.response import Response
|
||||
|
||||
from confspirator import groups
|
||||
from confspirator import fields
|
||||
|
||||
from adjutant.common import user_store
|
||||
from adjutant.api import models
|
||||
from adjutant.api import utils
|
||||
from adjutant.api.v1 import tasks
|
||||
from adjutant.api.v1.base import BaseDelegateAPI
|
||||
from adjutant.common.quota import QuotaManager
|
||||
from adjutant.config import CONF
|
||||
|
||||
|
||||
class UserList(tasks.InviteUser):
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
'blacklisted_roles',
|
||||
help_text="Users with any of these roles will be hidden from the user list.",
|
||||
default=[],
|
||||
sample_default=['admin']
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@utils.mod_or_admin
|
||||
def get(self, request):
|
||||
"""Get a list of all users who have been added to a project"""
|
||||
class_conf = settings.TASK_SETTINGS.get(
|
||||
'edit_user_roles', settings.DEFAULT_TASK_SETTINGS)
|
||||
role_blacklist = class_conf.get('role_blacklist', [])
|
||||
class_conf = self.config
|
||||
blacklisted_roles = class_conf.blacklisted_roles
|
||||
|
||||
user_list = []
|
||||
id_manager = user_store.IdentityManager()
|
||||
project_id = request.keystone_user['project_id']
|
||||
project = id_manager.get_project(project_id)
|
||||
|
||||
can_manage_roles = user_store.get_managable_roles(
|
||||
can_manage_roles = id_manager.get_manageable_roles(
|
||||
request.keystone_user['roles'])
|
||||
|
||||
active_emails = set()
|
||||
@ -47,7 +60,7 @@ class UserList(tasks.InviteUser):
|
||||
skip = False
|
||||
roles = []
|
||||
for role in user.roles:
|
||||
if role.name in role_blacklist:
|
||||
if role.name in blacklisted_roles:
|
||||
skip = True
|
||||
continue
|
||||
roles.append(role.name)
|
||||
@ -55,7 +68,7 @@ class UserList(tasks.InviteUser):
|
||||
continue
|
||||
inherited_roles = []
|
||||
for role in user.inherited_roles:
|
||||
if role.name in role_blacklist:
|
||||
if role.name in blacklisted_roles:
|
||||
skip = True
|
||||
continue
|
||||
inherited_roles.append(role.name)
|
||||
@ -81,7 +94,7 @@ class UserList(tasks.InviteUser):
|
||||
skip = False
|
||||
roles = []
|
||||
for role in user.roles:
|
||||
if role.name in role_blacklist:
|
||||
if role.name in blacklisted_roles:
|
||||
skip = True
|
||||
continue
|
||||
roles.append(role.name)
|
||||
@ -145,7 +158,7 @@ class UserList(tasks.InviteUser):
|
||||
'cohort': 'Invited',
|
||||
'status': task['status']
|
||||
}
|
||||
if not settings.USERNAME_IS_EMAIL:
|
||||
if not CONF.identity.username_is_email:
|
||||
user['name'] = task['task_data']['username']
|
||||
|
||||
user_list.append(user)
|
||||
@ -154,7 +167,17 @@ class UserList(tasks.InviteUser):
|
||||
|
||||
|
||||
class UserDetail(BaseDelegateAPI):
|
||||
task_type = 'edit_user_roles'
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
'blacklisted_roles',
|
||||
help_text="User with these roles will return not found.",
|
||||
default=[],
|
||||
sample_default=['admin']
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@utils.mod_or_admin
|
||||
def get(self, request, user_id):
|
||||
@ -170,18 +193,18 @@ class UserDetail(BaseDelegateAPI):
|
||||
if not user:
|
||||
return Response(no_user, status=404)
|
||||
|
||||
class_conf = settings.TASK_SETTINGS.get(
|
||||
self.task_type, settings.DEFAULT_TASK_SETTINGS)
|
||||
role_blacklist = class_conf.get('role_blacklist', [])
|
||||
class_conf = self.config
|
||||
blacklisted_roles = class_conf.blacklisted_roles
|
||||
|
||||
project_id = request.keystone_user['project_id']
|
||||
project = id_manager.get_project(project_id)
|
||||
|
||||
roles = [role.name for role in id_manager.get_roles(user, project)]
|
||||
roles_blacklisted = set(role_blacklist) & set(roles)
|
||||
roles_blacklisted = set(blacklisted_roles) & set(roles)
|
||||
inherited_roles = [
|
||||
role.name for role in id_manager.get_roles(user, project, True)]
|
||||
inherited_roles_blacklisted = (
|
||||
set(role_blacklist) & set(inherited_roles))
|
||||
set(blacklisted_roles) & set(inherited_roles))
|
||||
|
||||
if not roles or roles_blacklisted or inherited_roles_blacklisted:
|
||||
return Response(no_user, status=404)
|
||||
@ -221,7 +244,18 @@ class UserDetail(BaseDelegateAPI):
|
||||
|
||||
class UserRoles(BaseDelegateAPI):
|
||||
|
||||
task_type = 'edit_user_roles'
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
'blacklisted_roles',
|
||||
help_text="User with these roles will return not found.",
|
||||
default=[],
|
||||
sample_default=['admin']
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
task_type = "edit_user_roles"
|
||||
|
||||
@utils.mod_or_admin
|
||||
def get(self, request, user_id):
|
||||
@ -236,16 +270,15 @@ class UserRoles(BaseDelegateAPI):
|
||||
project_id = request.keystone_user['project_id']
|
||||
project = id_manager.get_project(project_id)
|
||||
|
||||
class_conf = settings.TASK_SETTINGS.get(
|
||||
self.task_type, settings.DEFAULT_TASK_SETTINGS)
|
||||
role_blacklist = class_conf.get('role_blacklist', [])
|
||||
class_conf = self.config
|
||||
blacklisted_roles = class_conf.blacklisted_roles
|
||||
|
||||
roles = [role.name for role in id_manager.get_roles(user, project)]
|
||||
roles_blacklisted = set(role_blacklist) & set(roles)
|
||||
roles_blacklisted = set(blacklisted_roles) & set(roles)
|
||||
inherited_roles = [
|
||||
role.name for role in id_manager.get_roles(user, project, True)]
|
||||
inherited_roles_blacklisted = (
|
||||
set(role_blacklist) & set(inherited_roles))
|
||||
set(blacklisted_roles) & set(inherited_roles))
|
||||
|
||||
if not roles or roles_blacklisted or inherited_roles_blacklisted:
|
||||
return Response(no_user, status=404)
|
||||
@ -290,18 +323,18 @@ class RoleList(BaseDelegateAPI):
|
||||
|
||||
# get roles for this user on the project
|
||||
user_roles = request.keystone_user['roles']
|
||||
managable_role_names = user_store.get_managable_roles(user_roles)
|
||||
|
||||
id_manager = user_store.IdentityManager()
|
||||
manageable_role_names = id_manager.get_manageable_roles(user_roles)
|
||||
|
||||
# look up role names and form output dict of valid roles
|
||||
managable_roles = []
|
||||
for role_name in managable_role_names:
|
||||
manageable_roles = []
|
||||
for role_name in manageable_role_names:
|
||||
role = id_manager.find_role(role_name)
|
||||
if role:
|
||||
managable_roles.append(role.to_dict())
|
||||
manageable_roles.append(role.to_dict())
|
||||
|
||||
return Response({'roles': managable_roles})
|
||||
return Response({'roles': manageable_roles})
|
||||
|
||||
|
||||
class UserResetPassword(tasks.ResetPassword):
|
||||
@ -387,8 +420,8 @@ class UpdateProjectQuotas(BaseDelegateAPI):
|
||||
as well as the current status of a specified region's quotas.
|
||||
"""
|
||||
|
||||
quota_settings = settings.PROJECT_QUOTA_SIZES
|
||||
size_order = settings.QUOTA_SIZES_ASC
|
||||
quota_sizes = CONF.quota.sizes
|
||||
size_order = CONF.quota.sizes_ascending
|
||||
|
||||
self.project_id = request.keystone_user['project_id']
|
||||
regions = request.query_params.get('regions', None)
|
||||
@ -416,7 +449,7 @@ class UpdateProjectQuotas(BaseDelegateAPI):
|
||||
response_tasks = self.get_active_quota_tasks()
|
||||
|
||||
return Response({'regions': region_quotas,
|
||||
"quota_sizes": quota_settings,
|
||||
"quota_sizes": quota_sizes,
|
||||
"quota_size_order": size_order,
|
||||
"active_quota_tasks": response_tasks})
|
||||
|
||||
|
@ -12,11 +12,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from rest_framework.response import Response
|
||||
|
||||
from confspirator import groups
|
||||
from confspirator import fields
|
||||
|
||||
from adjutant import exceptions
|
||||
from adjutant.api import utils
|
||||
from adjutant.api.v1.base import BaseDelegateAPI
|
||||
@ -27,6 +29,29 @@ from adjutant.api.v1.base import BaseDelegateAPI
|
||||
|
||||
class CreateProjectAndUser(BaseDelegateAPI):
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.StrConfig(
|
||||
'default_region',
|
||||
help_text="Default region in which any potential resources may be created.",
|
||||
required=True,
|
||||
default="RegionOne",
|
||||
),
|
||||
fields.StrConfig(
|
||||
"default_domain_id",
|
||||
help_text="Domain in which project and users will be created.",
|
||||
default="default",
|
||||
required=True,
|
||||
),
|
||||
fields.StrConfig(
|
||||
"default_parent_id",
|
||||
help_text="Parent id under which this project will be created. "
|
||||
"Default is None, and will create under default domain.",
|
||||
default=None,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
task_type = "create_project_and_user"
|
||||
|
||||
def post(self, request, format=None):
|
||||
@ -37,20 +62,19 @@ class CreateProjectAndUser(BaseDelegateAPI):
|
||||
incoming data and create a task to be approved
|
||||
later.
|
||||
"""
|
||||
self.logger.info("(%s) - Starting new project task." %
|
||||
timezone.now())
|
||||
self.logger.info(
|
||||
"(%s) - Starting new project task." % timezone.now())
|
||||
|
||||
class_conf = settings.TASK_SETTINGS.get(self.task_type, {})
|
||||
class_conf = self.config
|
||||
|
||||
# we need to set the region the resources will be created in:
|
||||
request.data['region'] = class_conf.get('default_region')
|
||||
request.data['region'] = class_conf.default_region
|
||||
|
||||
# domain
|
||||
request.data['domain_id'] = class_conf.get(
|
||||
'default_domain_id', 'default')
|
||||
request.data['domain_id'] = class_conf.default_domain_id
|
||||
|
||||
# parent_id for new project, if null defaults to domain:
|
||||
request.data['parent_id'] = class_conf.get('default_parent_id')
|
||||
request.data['parent_id'] = class_conf.default_parent_id
|
||||
|
||||
self.task_manager.create_from_request(self.task_type, request)
|
||||
|
||||
|
@ -12,25 +12,24 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import json
|
||||
import mock
|
||||
from unittest import skip
|
||||
|
||||
from django.utils import timezone
|
||||
from django.core import mail
|
||||
|
||||
import mock
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from confspirator.tests import utils as conf_utils
|
||||
|
||||
from adjutant.api.models import Task, Token, Notification
|
||||
from adjutant.common.tests import fake_clients
|
||||
from adjutant.common.tests.fake_clients import (
|
||||
FakeManager, setup_identity_cache)
|
||||
from adjutant.common.tests.utils import modify_dict_settings
|
||||
from adjutant.config import CONF
|
||||
|
||||
|
||||
@mock.patch('adjutant.common.user_store.IdentityManager',
|
||||
@ -77,7 +76,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -240,7 +239,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -271,7 +270,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -303,7 +302,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -366,7 +365,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -413,7 +412,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -441,7 +440,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -454,10 +453,13 @@ class AdminAPITests(APITestCase):
|
||||
self.assertEqual(response.json(),
|
||||
{"errors": ["No notification with this id."]})
|
||||
|
||||
@modify_dict_settings(TASK_SETTINGS={
|
||||
'key_list': ['create_project_and_user', 'notifications'],
|
||||
'operation': 'delete',
|
||||
})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.task_defaults.notifications.standard_handlers": [
|
||||
{'operation': 'override', 'value': []},
|
||||
],
|
||||
})
|
||||
def test_notification_acknowledge(self):
|
||||
"""
|
||||
Test that you can acknowledge a notification.
|
||||
@ -474,7 +476,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -514,7 +516,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -527,10 +529,13 @@ class AdminAPITests(APITestCase):
|
||||
{'errors':
|
||||
['No notification with this id.']})
|
||||
|
||||
@modify_dict_settings(TASK_SETTINGS={
|
||||
'key_list': ['create_project_and_user', 'notifications'],
|
||||
'operation': 'delete',
|
||||
})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.task_defaults.notifications.standard_handlers": [
|
||||
{'operation': 'override', 'value': []},
|
||||
],
|
||||
})
|
||||
def test_notification_re_acknowledge(self):
|
||||
"""
|
||||
Test that you cant reacknowledge a notification.
|
||||
@ -545,7 +550,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -564,10 +569,13 @@ class AdminAPITests(APITestCase):
|
||||
self.assertEqual(response.json(),
|
||||
{'notes': ['Notification already acknowledged.']})
|
||||
|
||||
@modify_dict_settings(TASK_SETTINGS={
|
||||
'key_list': ['create_project_and_user', 'notifications'],
|
||||
'operation': 'delete',
|
||||
})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.task_defaults.notifications.standard_handlers": [
|
||||
{'operation': 'override', 'value': []},
|
||||
],
|
||||
})
|
||||
def test_notification_acknowledge_no_data(self):
|
||||
"""
|
||||
Test that you have to include 'acknowledged': True to the request.
|
||||
@ -582,7 +590,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -613,7 +621,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -650,7 +658,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -667,35 +675,38 @@ class AdminAPITests(APITestCase):
|
||||
{u'notifications':
|
||||
[u'this field is required and needs to be a list.']})
|
||||
|
||||
@modify_dict_settings(DEFAULT_TASK_SETTINGS={
|
||||
'key_list': ['notifications'],
|
||||
'operation': 'override',
|
||||
'value': {
|
||||
'EmailNotification': {
|
||||
'standard': {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
'template': 'notification.txt'
|
||||
},
|
||||
'error': {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
'template': 'notification.txt'
|
||||
}
|
||||
}
|
||||
}
|
||||
}, TASK_SETTINGS={
|
||||
'key_list': ['create_project_and_user', 'emails'],
|
||||
'operation': 'override',
|
||||
'value': {
|
||||
'initial': None,
|
||||
'token': None,
|
||||
'completed': None
|
||||
}
|
||||
})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.create_project_and_user.notifications": [
|
||||
{'operation': 'override', 'value': {
|
||||
"standard_handlers": ["EmailNotification"],
|
||||
"error_handlers": ["EmailNotification"],
|
||||
"standard_handler_config": {
|
||||
"EmailNotification": {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
}
|
||||
},
|
||||
"error_handler_config": {
|
||||
"EmailNotification": {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
}
|
||||
},
|
||||
}},
|
||||
],
|
||||
"adjutant.workflow.tasks.create_project_and_user.emails": [
|
||||
{'operation': 'override', 'value': {
|
||||
'initial': None,
|
||||
'token': None,
|
||||
'completed': None
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_notification_email(self):
|
||||
"""
|
||||
Tests the email notification engine
|
||||
Tests the email notification handler
|
||||
"""
|
||||
setup_identity_cache()
|
||||
|
||||
@ -709,7 +720,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -769,7 +780,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -807,7 +818,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -836,12 +847,12 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -897,7 +908,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -936,7 +947,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -968,7 +979,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -996,7 +1007,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -1030,7 +1041,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -1068,7 +1079,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -1113,7 +1124,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -1145,12 +1156,12 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -1184,12 +1195,12 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -1214,20 +1225,20 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
data = {'email': "test2@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test2@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
data = {'email': "test3@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test3@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -1235,7 +1246,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -1258,20 +1269,20 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
data = {'email': "test2@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test2@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
data = {'email': "test3@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test3@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -1279,7 +1290,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -1306,16 +1317,16 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
data = {'email': "test2@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test2@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -1328,7 +1339,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -1385,12 +1396,12 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': project.name,
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "owner@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': 'test_project_id'}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@ -1398,7 +1409,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': project2.name,
|
||||
'project_id': project2.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -1425,7 +1436,7 @@ class AdminAPITests(APITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -1485,12 +1496,13 @@ class AdminAPITests(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@modify_dict_settings(TASK_SETTINGS={
|
||||
'key_list': ['reset_user_password', 'action_settings',
|
||||
'ResetUserPasswordAction', 'blacklisted_roles'],
|
||||
'operation': 'append',
|
||||
'value': ['admin']
|
||||
})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.action_defaults.ResetUserPasswordAction.blacklisted_roles": [
|
||||
{'operation': 'append', 'value': "admin"},
|
||||
],
|
||||
})
|
||||
def test_reset_admin(self):
|
||||
"""
|
||||
Ensure that you cannot issue a password reset for an
|
||||
@ -1539,13 +1551,12 @@ class AdminAPITests(APITestCase):
|
||||
new_task = Task.objects.all()[0]
|
||||
url = "/v1/tasks/" + new_task.uuid
|
||||
data = {
|
||||
'project_name': "test_project", 'email': "test@example.com",
|
||||
'region': 'test'
|
||||
'project_name': "test_project2", 'email': "test@example.com",
|
||||
}
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
|
@ -12,15 +12,15 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from datetime import timedelta
|
||||
import mock
|
||||
|
||||
from rest_framework import status
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.utils import modify_settings
|
||||
from django.test.utils import override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from confspirator.tests import utils as conf_utils
|
||||
|
||||
from adjutant.api.models import Token, Task
|
||||
from adjutant.common.tests import fake_clients
|
||||
from adjutant.common.tests.fake_clients import (
|
||||
@ -28,10 +28,8 @@ from adjutant.common.tests.fake_clients import (
|
||||
get_fake_cinderclient, get_fake_octaviaclient, cinder_cache, nova_cache,
|
||||
neutron_cache, octavia_cache, setup_mock_caches, setup_quota_cache,
|
||||
FakeResource)
|
||||
from adjutant.common.tests.utils import (
|
||||
modify_dict_settings, AdjutantAPITestCase)
|
||||
|
||||
from datetime import timedelta
|
||||
from adjutant.common.tests.utils import AdjutantAPITestCase
|
||||
from adjutant.config import CONF
|
||||
|
||||
|
||||
@mock.patch('adjutant.common.user_store.IdentityManager',
|
||||
@ -57,12 +55,12 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -86,13 +84,13 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -105,7 +103,7 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
url = "/v1/openstack/users"
|
||||
data = {'email': "test2@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test2@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -152,12 +150,12 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project3.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user3.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project3.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user3.id},
|
||||
inherited=True,
|
||||
),
|
||||
@ -176,7 +174,7 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project3.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -202,8 +200,8 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
self.assertEqual(u['roles'], ['project_mod'])
|
||||
|
||||
normal_user = project_users[0]
|
||||
self.assertEqual(normal_user['roles'], ['_member_', 'project_mod'])
|
||||
self.assertEqual(normal_user['inherited_roles'], ['_member_'])
|
||||
self.assertEqual(normal_user['roles'], ['member', 'project_mod'])
|
||||
self.assertEqual(normal_user['inherited_roles'], ['member'])
|
||||
|
||||
def test_user_detail(self):
|
||||
"""
|
||||
@ -218,13 +216,13 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
assignments = [
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id},
|
||||
inherited=True,
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
),
|
||||
]
|
||||
@ -235,7 +233,7 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -245,10 +243,10 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
response = self.client.get(url, headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.json()['username'], 'test@example.com')
|
||||
self.assertEqual(response.json()['roles'], ["_member_"])
|
||||
self.assertEqual(response.json()['inherited_roles'], ["_member_"])
|
||||
self.assertEqual(response.json()['roles'], ["member"])
|
||||
self.assertEqual(response.json()['inherited_roles'], ["member"])
|
||||
|
||||
def test_user_list_managable(self):
|
||||
def test_user_list_manageable(self):
|
||||
"""
|
||||
Confirm that the manageable value is set correctly.
|
||||
"""
|
||||
@ -265,7 +263,7 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
assignments = [
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
@ -275,7 +273,7 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user2.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
@ -293,7 +291,7 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "_member_,project_mod",
|
||||
'roles': "member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -319,7 +317,7 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
|
||||
assignment = fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
)
|
||||
|
||||
@ -329,7 +327,7 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
admin_headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -337,13 +335,19 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
|
||||
# admins removes role from the test user
|
||||
url = "/v1/openstack/users/%s/roles" % user.id
|
||||
data = {'roles': ["_member_"]}
|
||||
data = {'roles': ["member"]}
|
||||
response = self.client.delete(url, data,
|
||||
format='json', headers=admin_headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(response.json(), {'notes': ['task created']})
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_new_user_username_not_email(self):
|
||||
"""
|
||||
Ensure the new user workflow goes as expected.
|
||||
@ -357,12 +361,12 @@ class OpenstackAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id, 'username': 'user_name'}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -407,20 +411,20 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
extra_services = []
|
||||
|
||||
cinderquota = cinder_cache[region_name][project_id]['quota']
|
||||
gigabytes = settings.PROJECT_QUOTA_SIZES[size]['cinder']['gigabytes']
|
||||
gigabytes = CONF.quota.sizes[size]['cinder']['gigabytes']
|
||||
self.assertEqual(cinderquota['gigabytes'], gigabytes)
|
||||
|
||||
novaquota = nova_cache[region_name][project_id]['quota']
|
||||
ram = settings.PROJECT_QUOTA_SIZES[size]['nova']['ram']
|
||||
ram = CONF.quota.sizes[size]['nova']['ram']
|
||||
self.assertEqual(novaquota['ram'], ram)
|
||||
|
||||
neutronquota = neutron_cache[region_name][project_id]['quota']
|
||||
network = settings.PROJECT_QUOTA_SIZES[size]['neutron']['network']
|
||||
network = CONF.quota.sizes[size]['neutron']['network']
|
||||
self.assertEqual(neutronquota['network'], network)
|
||||
|
||||
if 'octavia' in extra_services:
|
||||
octaviaquota = octavia_cache[region_name][project_id]['quota']
|
||||
load_balancer = settings.PROJECT_QUOTA_SIZES.get(
|
||||
load_balancer = CONF.quota.sizes.get(
|
||||
size)['octavia']['load_balancer']
|
||||
self.assertEqual(octaviaquota['load_balancer'], load_balancer)
|
||||
|
||||
@ -438,7 +442,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
admin_headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -474,7 +478,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
admin_headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -506,7 +510,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "admin_project",
|
||||
'project_id': project.id,
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "admin",
|
||||
'user_id': "admin_id",
|
||||
'authenticated': True
|
||||
@ -542,7 +546,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
admin_headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -587,7 +591,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
admin_headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -640,7 +644,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -661,7 +665,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "second_project",
|
||||
'project_id': project2.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test2@example.com",
|
||||
'user_id': user.id,
|
||||
'authenticated': True
|
||||
@ -693,7 +697,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
admin_headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -717,7 +721,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "admin_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "admin",
|
||||
'user_id': "admin_id",
|
||||
'authenticated': True
|
||||
@ -752,7 +756,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
admin_headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': user.id,
|
||||
'authenticated': True
|
||||
@ -773,23 +777,29 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
self.assertEqual(
|
||||
response.data['regions'][0]['current_quota_size'], 'small')
|
||||
|
||||
@modify_dict_settings(PROJECT_QUOTA_SIZES=[
|
||||
{'key_list': ['zero'],
|
||||
'operation': 'override',
|
||||
'value':
|
||||
{'nova': {
|
||||
'instances': 0, 'cores': 0, 'ram': 0, 'floating_ips': 0,
|
||||
'fixed_ips': 0, 'metadata_items': 0, 'injected_files': 0,
|
||||
'injected_file_content_bytes': 0, 'key_pairs': 50,
|
||||
'security_groups': 0, 'security_group_rules': 0, },
|
||||
'cinder': {
|
||||
'gigabytes': 0, 'snapshots': 0, 'volumes': 0, },
|
||||
'neutron': {
|
||||
'floatingip': 0, 'network': 0, 'port': 0, 'router': 0,
|
||||
'security_group': 0, 'security_group_rule': 0}
|
||||
}
|
||||
}])
|
||||
@modify_settings(QUOTA_SIZES_ASC={'prepend': 'zero'})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.quota.sizes": [
|
||||
{'operation': 'update', 'value': {
|
||||
"zero": {
|
||||
'nova': {
|
||||
'instances': 0, 'cores': 0, 'ram': 0, 'floating_ips': 0,
|
||||
'fixed_ips': 0, 'metadata_items': 0, 'injected_files': 0,
|
||||
'injected_file_content_bytes': 0, 'key_pairs': 50,
|
||||
'security_groups': 0, 'security_group_rules': 0, },
|
||||
'cinder': {
|
||||
'gigabytes': 0, 'snapshots': 0, 'volumes': 0, },
|
||||
'neutron': {
|
||||
'floatingip': 0, 'network': 0, 'port': 0, 'router': 0,
|
||||
'security_group': 0, 'security_group_rule': 0}
|
||||
}
|
||||
}},
|
||||
],
|
||||
"adjutant.quota.sizes_ascending": [
|
||||
{'operation': 'prepend', 'value': "zero"},
|
||||
],
|
||||
})
|
||||
def test_calculate_quota_size_zero(self):
|
||||
"""
|
||||
Ensures that a zero quota enabled picks up
|
||||
@ -806,7 +816,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
admin_headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -866,7 +876,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -905,7 +915,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -938,7 +948,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -970,7 +980,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "admin_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "admin",
|
||||
'user_id': "admin_id",
|
||||
'authenticated': True
|
||||
@ -1007,7 +1017,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -1040,7 +1050,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "admin_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "admin",
|
||||
'user_id': "admin_id",
|
||||
'authenticated': True
|
||||
@ -1073,7 +1083,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -1148,7 +1158,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -1163,11 +1173,13 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@modify_dict_settings(TASK_SETTINGS=[
|
||||
{'key_list': ['update_quota', 'allow_auto_approve'],
|
||||
'operation': 'override',
|
||||
'value': False,
|
||||
}])
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.update_quota.allow_auto_approve": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_no_auto_approved_quota_change(self):
|
||||
""" Test allow_auto_approve config setting on a task."""
|
||||
|
||||
@ -1182,7 +1194,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
@ -1215,7 +1227,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
admin_headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': user.id,
|
||||
'authenticated': True
|
||||
@ -1230,13 +1242,13 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
response.data['regions'][0]['quota_change_options'], ['medium'])
|
||||
|
||||
cinder_cache['RegionOne'][project.id][
|
||||
'quota'] = settings.PROJECT_QUOTA_SIZES['large']['cinder']
|
||||
'quota'] = CONF.quota.sizes['large']['cinder']
|
||||
|
||||
nova_cache['RegionOne'][project.id][
|
||||
'quota'] = settings.PROJECT_QUOTA_SIZES['large']['nova']
|
||||
'quota'] = CONF.quota.sizes['large']['nova']
|
||||
|
||||
neutron_cache['RegionOne'][project.id][
|
||||
'quota'] = settings.PROJECT_QUOTA_SIZES['large']['neutron']
|
||||
'quota'] = CONF.quota.sizes['large']['neutron']
|
||||
|
||||
response = self.client.get(url, headers=admin_headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@ -1246,11 +1258,14 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
response.data['regions'][0]['quota_change_options'],
|
||||
['small', 'medium'])
|
||||
|
||||
@modify_dict_settings(QUOTA_SERVICES={
|
||||
'operation': 'append',
|
||||
'key_list': ['*'],
|
||||
'value': 'octavia'
|
||||
})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.quota.services": [
|
||||
{'operation': 'override', 'value': {
|
||||
'*': ['cinder', 'neutron', 'nova', 'octavia']}},
|
||||
],
|
||||
})
|
||||
def test_update_quota_no_history_with_octavia(self):
|
||||
""" Update quota for octavia."""
|
||||
|
||||
@ -1265,7 +1280,7 @@ class QuotaAPITests(AdjutantAPITestCase):
|
||||
admin_headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "user_id",
|
||||
'authenticated': True
|
||||
|
@ -14,20 +14,20 @@
|
||||
|
||||
import mock
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
|
||||
from rest_framework import status
|
||||
|
||||
from confspirator.tests import utils as conf_utils
|
||||
|
||||
from adjutant.api.models import Token, Notification
|
||||
from adjutant.tasks.models import Task
|
||||
from adjutant.tasks.v1.projects import CreateProjectAndUser
|
||||
from adjutant.common.tests.fake_clients import (
|
||||
FakeManager, setup_identity_cache)
|
||||
from adjutant.common.tests import fake_clients
|
||||
from adjutant.common.tests.utils import (AdjutantAPITestCase,
|
||||
modify_dict_settings)
|
||||
from adjutant.common.tests.utils import AdjutantAPITestCase
|
||||
from adjutant.config import CONF
|
||||
|
||||
|
||||
@mock.patch('adjutant.common.user_store.IdentityManager',
|
||||
@ -53,12 +53,12 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'wrong_email_field': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'wrong_email_field': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
@ -76,6 +76,14 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
'email': ['Enter a valid email address.'],
|
||||
'roles': ['"not_a_valid_role" is not a valid choice.']}})
|
||||
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.invite_user_to_project.emails": [
|
||||
{'operation': 'update', 'value': {
|
||||
"initial": None, "token": {"subject": "invite_user_to_project"}}},
|
||||
],
|
||||
})
|
||||
def test_new_user(self):
|
||||
"""
|
||||
Ensure the new user workflow goes as expected.
|
||||
@ -89,12 +97,12 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -123,12 +131,12 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': 'test_project_id'}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
@ -145,12 +153,12 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "_member_",
|
||||
'roles': "member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': 'test_project_id'}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
@ -164,7 +172,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
|
||||
url = "/v1/actions/InviteUser"
|
||||
headers = {}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': 'test_project_id'}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
@ -188,12 +196,12 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -219,7 +227,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
|
||||
assignment = fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
)
|
||||
|
||||
@ -230,12 +238,12 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -262,7 +270,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -286,6 +294,20 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.create_project_and_user.notifications": [
|
||||
{'operation': 'override', 'value': {
|
||||
"standard_handler_config": {
|
||||
"EmailNotification": {
|
||||
'emails': ['example_notification@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
}
|
||||
}
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_new_project_invalid_on_submit(self):
|
||||
"""
|
||||
Ensures that when a project becomes invalid at the submit stage
|
||||
@ -302,7 +324,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -345,7 +367,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -380,7 +402,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "admin_project",
|
||||
'project_id': "admin_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "admin",
|
||||
'user_id': "admin_id",
|
||||
'authenticated': True
|
||||
@ -415,7 +437,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "admin_project",
|
||||
'project_id': "admin_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "admin",
|
||||
'user_id': "admin_id",
|
||||
'authenticated': True
|
||||
@ -544,7 +566,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
@ -587,12 +609,12 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -600,7 +622,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
|
||||
|
||||
data = {'email': "test2@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test2@example.com", 'roles': ["member"],
|
||||
'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -623,7 +645,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': user.id,
|
||||
'authenticated': True
|
||||
@ -643,21 +665,29 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(user.name, 'new_test@example.com')
|
||||
|
||||
@modify_dict_settings(TASK_SETTINGS=[
|
||||
{'key_list': ['update_user_email', 'additional_actions'],
|
||||
'operation': 'append',
|
||||
'value': ['SendAdditionalEmailAction']},
|
||||
{'key_list': ['update_user_email', 'action_settings',
|
||||
'SendAdditionalEmailAction', 'initial'],
|
||||
'operation': 'update',
|
||||
'value': {
|
||||
'subject': 'update_user_email_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_roles': [],
|
||||
'email_current_user': True,
|
||||
}
|
||||
}
|
||||
])
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.update_user_email.additional_actions": [
|
||||
{'operation': 'append', 'value': "SendAdditionalEmailAction"},
|
||||
],
|
||||
"adjutant.workflow.tasks.update_user_email.emails": [
|
||||
{'operation': 'update', 'value': {
|
||||
"initial": None, "token": {"subject": "update_user_email_token"}}},
|
||||
],
|
||||
"adjutant.workflow.tasks.update_user_email.actions": [
|
||||
{'operation': 'update', 'value': {
|
||||
"SendAdditionalEmailAction": {
|
||||
"prepare": {
|
||||
'subject': 'update_user_email_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_roles': [],
|
||||
'email_current_user': True,
|
||||
}
|
||||
}
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_update_email_task_send_email_to_current_user(self):
|
||||
"""
|
||||
Tests the email update workflow, and ensures that when setup
|
||||
@ -673,7 +703,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': user.id,
|
||||
'authenticated': True
|
||||
@ -686,6 +716,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
self.assertEqual(response.data, {'notes': ['task created']})
|
||||
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
|
||||
self.assertEqual(mail.outbox[0].to, ['test@example.com'])
|
||||
self.assertEqual(
|
||||
mail.outbox[0].subject, 'update_user_email_additional')
|
||||
@ -703,21 +734,32 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
|
||||
self.assertEqual(len(mail.outbox), 3)
|
||||
|
||||
@modify_dict_settings(TASK_SETTINGS=[
|
||||
{'key_list': ['update_user_email', 'additional_actions'],
|
||||
'operation': 'append',
|
||||
'value': ['SendAdditionalEmailAction']},
|
||||
{'key_list': ['update_user_email', 'action_settings',
|
||||
'SendAdditionalEmailAction', 'initial'],
|
||||
'operation': 'update',
|
||||
'value': {
|
||||
'subject': 'update_user_email_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_roles': [],
|
||||
'email_current_user': True}
|
||||
}
|
||||
])
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.update_user_email.additional_actions": [
|
||||
{'operation': 'append', 'value': "SendAdditionalEmailAction"},
|
||||
],
|
||||
"adjutant.workflow.tasks.update_user_email.emails": [
|
||||
{'operation': 'update', 'value': {
|
||||
"initial": None, "token": {"subject": "update_user_email_token"}}},
|
||||
],
|
||||
"adjutant.workflow.tasks.update_user_email.actions": [
|
||||
{'operation': 'update', 'value': {
|
||||
"SendAdditionalEmailAction": {
|
||||
"prepare": {
|
||||
'subject': 'update_user_email_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_roles': [],
|
||||
'email_current_user': True,
|
||||
}
|
||||
}
|
||||
}},
|
||||
],
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_update_email_task_send_email_current_name_not_email(self):
|
||||
"""
|
||||
Tests the email update workflow when USERNAME_IS_EMAIL=False, and
|
||||
@ -734,7 +776,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "nkdfslnkls",
|
||||
'user_id': user.id,
|
||||
'authenticated': True,
|
||||
@ -775,7 +817,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': user.id,
|
||||
'authenticated': True
|
||||
@ -789,7 +831,16 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
response.json(),
|
||||
{'errors': {'new_email': [u'Enter a valid email address.']}})
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=True)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
"adjutant.workflow.tasks.update_user_email.emails": [
|
||||
{'operation': 'update', 'value': {"initial": None}},
|
||||
],
|
||||
})
|
||||
def test_update_email_pre_existing_user_with_email(self):
|
||||
|
||||
user = fake_clients.FakeUser(
|
||||
@ -805,7 +856,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True,
|
||||
@ -821,7 +872,16 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
"adjutant.workflow.tasks.update_user_email.emails": [
|
||||
{'operation': 'update', 'value': {"initial": None}},
|
||||
],
|
||||
})
|
||||
def test_update_email_user_with_email_username_not_email(self):
|
||||
|
||||
user = fake_clients.FakeUser(
|
||||
@ -837,7 +897,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': user.id,
|
||||
'authenticated': True
|
||||
@ -878,7 +938,13 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_update_email_task_username_not_email(self):
|
||||
|
||||
user = fake_clients.FakeUser(
|
||||
@ -890,7 +956,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test_user",
|
||||
'user_id': user.id,
|
||||
'authenticated': True
|
||||
@ -912,7 +978,17 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
self.assertEqual(user.email, 'new_test@example.com')
|
||||
|
||||
# Tests for USERNAME_IS_EMAIL=False
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
"adjutant.workflow.tasks.invite_user_to_project.emails": [
|
||||
{'operation': 'update', 'value': {
|
||||
"initial": None, "token": {"subject": "invite_user_to_project"}}},
|
||||
],
|
||||
})
|
||||
def test_invite_user_to_project_email_not_username(self):
|
||||
"""
|
||||
Invites a user where the email is different to the username.
|
||||
@ -925,13 +1001,13 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "user",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'username': 'new_user', 'email': "new@example.com",
|
||||
'roles': ["_member_"], 'project_id': project.id}
|
||||
'roles': ["member"], 'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(response.json(), {'notes': ['task created']})
|
||||
@ -951,7 +1027,17 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
fake_clients.identity_cache['new_users'][0].name,
|
||||
'new_user')
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
"adjutant.workflow.tasks.reset_user_password.emails": [
|
||||
{'operation': 'update', 'value': {
|
||||
"initial": None, "token": {"subject": "Password Reset for OpenStack"}}},
|
||||
],
|
||||
})
|
||||
def test_reset_user_username_not_email(self):
|
||||
"""
|
||||
Ensure the reset user workflow goes as expected.
|
||||
@ -991,7 +1077,13 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(user.password, 'new_test_password')
|
||||
|
||||
@override_settings(USERNAME_IS_EMAIL=False)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.username_is_email": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_new_project_username_not_email(self):
|
||||
setup_identity_cache()
|
||||
|
||||
@ -1030,22 +1122,27 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
@modify_dict_settings(
|
||||
TASK_SETTINGS=[
|
||||
{'key_list': ['invite_user_to_project', 'additional_actions'],
|
||||
'operation': 'append',
|
||||
'value': ['SendAdditionalEmailAction']},
|
||||
{'key_list': ['invite_user_to_project', 'action_settings',
|
||||
'SendAdditionalEmailAction', 'initial'],
|
||||
'operation': 'update',
|
||||
'value': {
|
||||
'subject': 'update_user_email_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_roles': ['project_admin'],
|
||||
'email_current_user': False,
|
||||
}
|
||||
}
|
||||
])
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.invite_user_to_project.additional_actions": [
|
||||
{'operation': 'append', 'value': "SendAdditionalEmailAction"},
|
||||
],
|
||||
"adjutant.workflow.tasks.invite_user_to_project.emails": [
|
||||
{'operation': 'update', 'value': {"initial": None}},
|
||||
],
|
||||
"adjutant.workflow.tasks.invite_user_to_project.actions": [
|
||||
{'operation': 'update', 'value': {
|
||||
"SendAdditionalEmailAction": {
|
||||
"prepare": {
|
||||
'subject': 'invite_user_to_project_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_roles': ['project_admin'],
|
||||
}
|
||||
}
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_additional_emails_roles(self):
|
||||
"""
|
||||
Tests the sending of additional emails to a set of roles in a project
|
||||
@ -1072,7 +1169,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
assignments = [
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
@ -1082,7 +1179,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user2.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
@ -1092,7 +1189,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user3.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
@ -1110,14 +1207,14 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
|
||||
data = {'email': "new_test@example.com",
|
||||
'roles': ['_member_'], 'project_id': project.id}
|
||||
'roles': ['member'], 'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(response.json(), {'notes': ['task created']})
|
||||
@ -1128,7 +1225,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
self.assertEqual(set(mail.outbox[0].to),
|
||||
set([user.email, user2.email]))
|
||||
self.assertEqual(
|
||||
mail.outbox[0].subject, 'update_user_email_additional')
|
||||
mail.outbox[0].subject, 'invite_user_to_project_additional')
|
||||
|
||||
# Test that the token email gets sent to the other addresses
|
||||
self.assertEqual(mail.outbox[1].to[0], 'new_test@example.com')
|
||||
@ -1140,22 +1237,28 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
@modify_dict_settings(
|
||||
TASK_SETTINGS=[
|
||||
{'key_list': ['invite_user_to_project', 'additional_actions'],
|
||||
'operation': 'append',
|
||||
'value': ['SendAdditionalEmailAction']},
|
||||
{'key_list': ['invite_user_to_project', 'action_settings',
|
||||
'SendAdditionalEmailAction', 'initial'],
|
||||
'operation': 'update',
|
||||
'value': {
|
||||
'subject': 'update_user_email_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_roles': ['project_admin'],
|
||||
'email_current_user': False,
|
||||
}
|
||||
}
|
||||
])
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.invite_user_to_project.additional_actions": [
|
||||
{'operation': 'append', 'value': "SendAdditionalEmailAction"},
|
||||
],
|
||||
"adjutant.workflow.tasks.invite_user_to_project.emails": [
|
||||
{'operation': 'update', 'value': {
|
||||
"initial": None, "token": {"subject": "invite_user_to_project_token"}}},
|
||||
],
|
||||
"adjutant.workflow.tasks.invite_user_to_project.actions": [
|
||||
{'operation': 'update', 'value': {
|
||||
"SendAdditionalEmailAction": {
|
||||
"prepare": {
|
||||
'subject': 'invite_user_to_project_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_roles': ['project_admin'],
|
||||
}
|
||||
}
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_additional_emails_role_no_email(self):
|
||||
"""
|
||||
Tests that setting email roles to something that has no people to
|
||||
@ -1169,7 +1272,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
|
||||
assignment = fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
)
|
||||
|
||||
@ -1180,14 +1283,14 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
|
||||
data = {'email': "new_test@example.com",
|
||||
'roles': ['_member_']}
|
||||
'roles': ['member']}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(response.data, {'notes': ['task created']})
|
||||
@ -1204,22 +1307,27 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
@modify_dict_settings(
|
||||
TASK_SETTINGS=[
|
||||
{'key_list': ['invite_user_to_project', 'additional_actions'],
|
||||
'operation': 'override',
|
||||
'value': ['SendAdditionalEmailAction']},
|
||||
{'key_list': ['invite_user_to_project', 'action_settings',
|
||||
'SendAdditionalEmailAction', 'initial'],
|
||||
'operation': 'update',
|
||||
'value':{
|
||||
'subject': 'invite_user_to_project_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_additional_addresses': ['admin@example.com'],
|
||||
'email_current_user': False,
|
||||
}
|
||||
}
|
||||
])
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.invite_user_to_project.additional_actions": [
|
||||
{'operation': 'append', 'value': "SendAdditionalEmailAction"},
|
||||
],
|
||||
"adjutant.workflow.tasks.invite_user_to_project.emails": [
|
||||
{'operation': 'update', 'value': {"initial": None}},
|
||||
],
|
||||
"adjutant.workflow.tasks.invite_user_to_project.actions": [
|
||||
{'operation': 'update', 'value': {
|
||||
"SendAdditionalEmailAction": {
|
||||
"prepare": {
|
||||
'subject': 'invite_user_to_project_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_additional_addresses': ['admin@example.com'],
|
||||
}
|
||||
}
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_email_additional_addresses(self):
|
||||
"""
|
||||
Tests the sending of additional emails an admin email set in
|
||||
@ -1233,7 +1341,7 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
assignments = [
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="_member_",
|
||||
role_name="member",
|
||||
user={'id': user.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
@ -1250,13 +1358,13 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
|
||||
data = {'email': "new_test@example.com", 'roles': ['_member_']}
|
||||
data = {'email': "new_test@example.com", 'roles': ['member']}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
@ -1278,22 +1386,28 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
@modify_dict_settings(
|
||||
TASK_SETTINGS=[
|
||||
{'key_list': ['invite_user_to_project', 'additional_actions'],
|
||||
'operation': 'override',
|
||||
'value': ['SendAdditionalEmailAction']},
|
||||
{'key_list': ['invite_user_to_project', 'action_settings',
|
||||
'SendAdditionalEmailAction', 'initial'],
|
||||
'operation': 'update',
|
||||
'value':{
|
||||
'subject': 'invite_user_to_project_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_additional_addresses': ['admin@example.com'],
|
||||
'email_current_user': False,
|
||||
}
|
||||
}
|
||||
])
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.invite_user_to_project.additional_actions": [
|
||||
{'operation': 'append', 'value': "SendAdditionalEmailAction"},
|
||||
],
|
||||
"adjutant.workflow.tasks.invite_user_to_project.emails": [
|
||||
{'operation': 'update', 'value': {
|
||||
"initial": None, "token": {"subject": "invite_user_to_project_token"}}},
|
||||
],
|
||||
"adjutant.workflow.tasks.invite_user_to_project.actions": [
|
||||
{'operation': 'update', 'value': {
|
||||
"SendAdditionalEmailAction": {
|
||||
"prepare": {
|
||||
'subject': 'invite_user_to_project_additional',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_additional_addresses': ['admin@example.com'],
|
||||
}
|
||||
}
|
||||
}},
|
||||
],
|
||||
})
|
||||
def test_email_additional_action_invalid(self):
|
||||
"""
|
||||
The additional email actions should not send an email if the
|
||||
@ -1306,12 +1420,12 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'email': "test@example.com", 'roles': ["_member_"],
|
||||
data = {'email': "test@example.com", 'roles': ["member"],
|
||||
'project_id': 'test_project_id'}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1339,12 +1453,9 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
|
||||
new_task = Task.objects.all()[0]
|
||||
|
||||
class_conf = settings.TASK_SETTINGS.get(
|
||||
CreateProjectAndUser.task_type, settings.DEFAULT_TASK_SETTINGS)
|
||||
expected_action_names = (
|
||||
class_conf.get('default_actions', [])
|
||||
or CreateProjectAndUser.default_actions[:])
|
||||
expected_action_names += class_conf.get('additional_actions', [])
|
||||
class_conf = new_task.config
|
||||
expected_action_names = CreateProjectAndUser.default_actions[:]
|
||||
expected_action_names += class_conf.additional_actions
|
||||
|
||||
actions = new_task.actions
|
||||
observed_action_names = [a.action_name for a in actions]
|
||||
@ -1381,7 +1492,13 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
"task. See task itself for details."]})
|
||||
self.assertEqual(new_notification.task, new_task)
|
||||
|
||||
@override_settings(KEYSTONE={'can_edit_users': False})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.can_edit_users": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_user_invite_cant_edit_users(self):
|
||||
"""
|
||||
When can_edit_users is false, and a new user is invited,
|
||||
@ -1396,18 +1513,24 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "user",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'username': 'new_user', 'email': "new@example.com",
|
||||
'roles': ["_member_"], 'project_id': project.id}
|
||||
'roles': ["member"], 'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.json(), {'errors': ['actions invalid']})
|
||||
|
||||
@override_settings(KEYSTONE={'can_edit_users': False})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.can_edit_users": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_user_invite_cant_edit_users_existing_user(self):
|
||||
"""
|
||||
When can_edit_users is false, and a new user is invited,
|
||||
@ -1423,18 +1546,24 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'roles': "project_admin,member,project_mod",
|
||||
'username': "user",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
data = {'username': 'new_user', 'email': "test@example.com",
|
||||
'roles': ["_member_"], 'project_id': project.id}
|
||||
'roles': ["member"], 'project_id': project.id}
|
||||
response = self.client.post(url, data, format='json', headers=headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(response.json(), {'notes': ['task created']})
|
||||
|
||||
@override_settings(KEYSTONE={'can_edit_users': False})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.can_edit_users": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_project_create_cant_edit_users(self):
|
||||
"""
|
||||
When can_edit_users is false, and a new signup comes in,
|
||||
@ -1456,7 +1585,13 @@ class DelegateAPITests(AdjutantAPITestCase):
|
||||
actions = [act.get_action() for act in action_models]
|
||||
self.assertFalse(all([act.valid for act in actions]))
|
||||
|
||||
@override_settings(KEYSTONE={'can_edit_users': False})
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.identity.can_edit_users": [
|
||||
{'operation': 'override', 'value': False},
|
||||
],
|
||||
})
|
||||
def test_project_create_cant_edit_users_existing_user(self):
|
||||
"""
|
||||
When can_edit_users is false, and a new signup comes in,
|
||||
|
@ -14,8 +14,9 @@
|
||||
|
||||
from django.conf.urls import url
|
||||
from adjutant.api.v1 import views
|
||||
from django.conf import settings
|
||||
|
||||
from adjutant import api
|
||||
from adjutant.config import CONF
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^status/?$', views.StatusView.as_view()),
|
||||
@ -28,8 +29,8 @@ urlpatterns = [
|
||||
url(r'^notifications/?$', views.NotificationList.as_view()),
|
||||
]
|
||||
|
||||
for active_view in settings.ACTIVE_DELEGATE_APIS:
|
||||
delegate_api = settings.DELEGATE_API_CLASSES[active_view]
|
||||
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())
|
||||
|
@ -16,44 +16,10 @@ import json
|
||||
|
||||
from decorator import decorator
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import FieldError
|
||||
|
||||
from rest_framework.response import Response
|
||||
|
||||
from adjutant.api.models import Notification
|
||||
|
||||
|
||||
# TODO(adriant): move this to 'adjutant.notifications.utils'
|
||||
def create_notification(task, notes, error=False, engines=True):
|
||||
notification = Notification.objects.create(
|
||||
task=task,
|
||||
notes=notes,
|
||||
error=error
|
||||
)
|
||||
notification.save()
|
||||
|
||||
if not engines:
|
||||
return notification
|
||||
|
||||
class_conf = settings.TASK_SETTINGS.get(
|
||||
task.task_type, settings.DEFAULT_TASK_SETTINGS)
|
||||
|
||||
notification_conf = class_conf.get('notifications', {})
|
||||
|
||||
if notification_conf:
|
||||
for note_engine, conf in notification_conf.items():
|
||||
if error:
|
||||
conf = conf.get('error', {})
|
||||
else:
|
||||
conf = conf.get('standard', {})
|
||||
if not conf:
|
||||
continue
|
||||
engine = settings.NOTIFICATION_ENGINES[note_engine](conf)
|
||||
engine.notify(task, notification)
|
||||
|
||||
return notification
|
||||
|
||||
|
||||
# "{'filters': {'fieldname': { 'operation': 'value'}}
|
||||
@decorator
|
||||
@ -92,13 +58,3 @@ def parse_filters(func, *args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
except FieldError as e:
|
||||
return Response({'errors': [str(e)]}, status=400)
|
||||
|
||||
|
||||
def add_task_id_for_roles(request, processed, response_dict, req_roles):
|
||||
if request.keystone_user.get('authenticated', False):
|
||||
|
||||
req_roles = set(req_roles)
|
||||
roles = set(request.keystone_user.get('roles', []))
|
||||
|
||||
if roles & req_roles:
|
||||
response_dict['task'] = processed['task'].uuid
|
||||
|
0
adjutant/commands/__init__.py
Normal file
0
adjutant/commands/__init__.py
Normal file
0
adjutant/commands/management/__init__.py
Normal file
0
adjutant/commands/management/__init__.py
Normal file
0
adjutant/commands/management/commands/__init__.py
Normal file
0
adjutant/commands/management/commands/__init__.py
Normal file
89
adjutant/commands/management/commands/exampleconfig.py
Normal file
89
adjutant/commands/management/commands/exampleconfig.py
Normal file
@ -0,0 +1,89 @@
|
||||
import yaml
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from confspirator import groups
|
||||
|
||||
from adjutant import config
|
||||
|
||||
|
||||
def make_yaml_lines(val, depth, comment=False):
|
||||
new_lines = []
|
||||
line_prefix = " " * (depth + 1)
|
||||
for line in yaml.dump(val).split('\n'):
|
||||
if line == '':
|
||||
continue
|
||||
if comment:
|
||||
new_lines.append(line_prefix + "# %s" % line)
|
||||
else:
|
||||
new_lines.append(line_prefix + line)
|
||||
return new_lines
|
||||
|
||||
|
||||
def make_field_lines(field, depth):
|
||||
field_lines = []
|
||||
line_prefix = " " * (depth + 1)
|
||||
field_type = field.type.__class__.__name__
|
||||
field_lines.append(line_prefix + "# %s" % field_type)
|
||||
field_help_text = "# %s" % field.help_text
|
||||
field_lines.append(line_prefix + field_help_text)
|
||||
|
||||
default = ''
|
||||
if field.default is not None:
|
||||
default = field.default
|
||||
|
||||
if not default and field.sample_default is not None:
|
||||
default = field.sample_default
|
||||
|
||||
if field_type == "Dict":
|
||||
if default:
|
||||
field_lines.append(line_prefix + "%s:" % field.name)
|
||||
field_lines += make_yaml_lines(default, depth + 1)
|
||||
else:
|
||||
field_lines.append(line_prefix + "# %s:" % field.name)
|
||||
elif field_type == "List":
|
||||
if default:
|
||||
field_lines.append(line_prefix + "%s:" % field.name)
|
||||
field_lines += make_yaml_lines(default, depth + 1)
|
||||
else:
|
||||
field_lines.append(line_prefix + "# %s:" % field.name)
|
||||
else:
|
||||
if default == '':
|
||||
field_lines.append(line_prefix + "# %s: <your_value>" % field.name)
|
||||
else:
|
||||
default_str = " " + str(default)
|
||||
field_lines.append(line_prefix + "%s:%s" % (field.name, default_str))
|
||||
return field_lines
|
||||
|
||||
|
||||
def make_group_lines(group, depth=0):
|
||||
group_lines = []
|
||||
line_prefix = " " * depth
|
||||
group_lines.append(line_prefix + "%s:" % group.name)
|
||||
|
||||
for child in group:
|
||||
if isinstance(child, groups.ConfigGroup):
|
||||
group_lines += make_group_lines(child, depth=depth + 1)
|
||||
else:
|
||||
group_lines += make_field_lines(child, depth)
|
||||
return group_lines
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = ''
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--output-file', default="adjutant.yaml")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
print("Generating example file to: '%s'" % options['output_file'])
|
||||
|
||||
base_lines = []
|
||||
for group in config._root_config:
|
||||
base_lines += make_group_lines(group)
|
||||
base_lines.append("")
|
||||
|
||||
with open(options['output_file'], "w") as f:
|
||||
for line in base_lines:
|
||||
f.write(line)
|
||||
f.write("\n")
|
@ -15,3 +15,5 @@
|
||||
# Date formats to use when storing time data we expect to parse.
|
||||
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
||||
DATE_FORMAT_MS = "%Y-%m-%dT%H:%M:%S.%f"
|
||||
EMAIL_REGEX = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
|
||||
EMAIL_WITH_TEMPLATE_REGEX = r"(^[%()a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
|
||||
|
@ -13,8 +13,6 @@
|
||||
# under the License.
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from keystoneauth1.identity import v3
|
||||
from keystoneauth1 import session
|
||||
from keystoneclient import client as ks_client
|
||||
@ -24,6 +22,8 @@ from neutronclient.v2_0 import client as neutronclient
|
||||
from novaclient import client as novaclient
|
||||
from octaviaclient.api.v2 import octavia
|
||||
|
||||
from adjutant.config import CONF
|
||||
|
||||
# Defined for use locally
|
||||
DEFAULT_COMPUTE_VERSION = "2"
|
||||
DEFAULT_IDENTITY_VERSION = "3"
|
||||
@ -43,12 +43,12 @@ def get_auth_session():
|
||||
if not client_auth_session:
|
||||
|
||||
auth = v3.Password(
|
||||
username=settings.KEYSTONE['username'],
|
||||
password=settings.KEYSTONE['password'],
|
||||
project_name=settings.KEYSTONE['project_name'],
|
||||
auth_url=settings.KEYSTONE['auth_url'],
|
||||
user_domain_id=settings.KEYSTONE.get('domain_id', "default"),
|
||||
project_domain_id=settings.KEYSTONE.get('domain_id', "default"),
|
||||
username=CONF.identity.auth.username,
|
||||
password=CONF.identity.auth.password,
|
||||
project_name=CONF.identity.auth.project_name,
|
||||
auth_url=CONF.identity.auth.auth_url,
|
||||
user_domain_id=CONF.identity.auth.user_domain_id,
|
||||
project_domain_id=CONF.identity.auth.project_domain_id,
|
||||
)
|
||||
client_auth_session = session.Session(auth=auth)
|
||||
|
||||
|
@ -12,11 +12,9 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from adjutant.config import CONF
|
||||
from adjutant.common import openstack_clients
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class QuotaManager(object):
|
||||
"""
|
||||
@ -182,23 +180,22 @@ class QuotaManager(object):
|
||||
self.default_helpers = dict(self._quota_updaters)
|
||||
self.helpers = {}
|
||||
|
||||
if settings.QUOTA_SERVICES:
|
||||
quota_services = dict(settings.QUOTA_SERVICES)
|
||||
quota_services = dict(CONF.quota.services)
|
||||
|
||||
all_regions = quota_services.pop('*', None)
|
||||
if all_regions:
|
||||
self.default_helpers = {}
|
||||
for service in all_regions:
|
||||
if service in self._quota_updaters:
|
||||
self.default_helpers[service] = \
|
||||
self._quota_updaters[service]
|
||||
all_regions = quota_services.pop('*', None)
|
||||
if all_regions:
|
||||
self.default_helpers = {}
|
||||
for service in all_regions:
|
||||
if service in self._quota_updaters:
|
||||
self.default_helpers[service] = \
|
||||
self._quota_updaters[service]
|
||||
|
||||
for region, services in quota_services.items():
|
||||
self.helpers[region] = {}
|
||||
for service in services:
|
||||
if service in self._quota_updaters:
|
||||
self.helpers[region][service] = \
|
||||
self._quota_updaters[service]
|
||||
for region, services in quota_services.items():
|
||||
self.helpers[region] = {}
|
||||
for service in services:
|
||||
if service in self._quota_updaters:
|
||||
self.helpers[region][service] = \
|
||||
self._quota_updaters[service]
|
||||
|
||||
self.project_id = project_id
|
||||
self.size_diff_threshold = (size_difference_threshold
|
||||
@ -217,7 +214,7 @@ class QuotaManager(object):
|
||||
def get_quota_differences(self, current_quota):
|
||||
""" Gets the closest matching quota size for a given quota """
|
||||
quota_differences = {}
|
||||
for size, setting in settings.PROJECT_QUOTA_SIZES.items():
|
||||
for size, setting in CONF.quota.sizes.items():
|
||||
match_percentages = []
|
||||
for service_name, values in setting.items():
|
||||
if service_name not in current_quota:
|
||||
@ -268,7 +265,7 @@ class QuotaManager(object):
|
||||
|
||||
def get_quota_change_options(self, quota_size):
|
||||
""" Get's the pre-approved quota change options for a given size """
|
||||
quota_list = settings.QUOTA_SIZES_ASC
|
||||
quota_list = CONF.quota.sizes_ascending
|
||||
try:
|
||||
list_position = quota_list.index(quota_size)
|
||||
except ValueError:
|
||||
@ -283,7 +280,7 @@ class QuotaManager(object):
|
||||
|
||||
def get_smaller_quota_options(self, quota_size):
|
||||
""" Get the quota sizes smaller than the current size."""
|
||||
quota_list = settings.QUOTA_SIZES_ASC
|
||||
quota_list = CONF.quota.sizes_ascending
|
||||
try:
|
||||
list_position = quota_list.index(quota_size)
|
||||
except ValueError:
|
||||
|
@ -14,10 +14,10 @@
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import mock
|
||||
|
||||
from adjutant.config import CONF
|
||||
|
||||
|
||||
identity_cache = {}
|
||||
neutron_cache = {}
|
||||
@ -102,7 +102,6 @@ def setup_identity_cache(projects=None, users=None, role_assignments=None,
|
||||
credentials=None, extra_roles=None):
|
||||
if extra_roles is None:
|
||||
extra_roles = []
|
||||
|
||||
if not projects:
|
||||
projects = []
|
||||
if not users:
|
||||
@ -125,7 +124,7 @@ def setup_identity_cache(projects=None, users=None, role_assignments=None,
|
||||
users.append(admin_user)
|
||||
|
||||
roles = [
|
||||
FakeRole(name="_member_"),
|
||||
FakeRole(name="member"),
|
||||
FakeRole(name="admin"),
|
||||
FakeRole(name="project_admin"),
|
||||
FakeRole(name="project_mod"),
|
||||
@ -164,7 +163,7 @@ class FakeManager(object):
|
||||
def __init__(self):
|
||||
# TODO(adriant): decide if we want to have some function calls
|
||||
# throw errors if this is false.
|
||||
self.can_edit_users = settings.KEYSTONE.get('can_edit_users', True)
|
||||
self.can_edit_users = CONF.identity.can_edit_users
|
||||
|
||||
def _project_from_id(self, project):
|
||||
if isinstance(project, FakeProject):
|
||||
@ -482,6 +481,31 @@ class FakeManager(object):
|
||||
for cred in found:
|
||||
identity_cache['credentials'].remove(cred)
|
||||
|
||||
# TODO(adriant): Move this to a BaseIdentityManager class when
|
||||
# it exists.
|
||||
def get_manageable_roles(self, user_roles=None):
|
||||
"""Get roles which can be managed
|
||||
|
||||
Given a list of user role names, returns a list of names
|
||||
that the user is allowed to manage.
|
||||
|
||||
If user_roles is not given, returns all possible roles.
|
||||
"""
|
||||
roles_mapping = CONF.identity.role_mapping
|
||||
if user_roles is None:
|
||||
all_roles = []
|
||||
for options in roles_mapping.values():
|
||||
all_roles += options
|
||||
return list(set(all_roles))
|
||||
|
||||
# merge mapping lists to form a flat permitted roles list
|
||||
manageable_role_names = [mrole for role_name in user_roles
|
||||
if role_name in roles_mapping
|
||||
for mrole in roles_mapping[role_name]]
|
||||
# a set has unique items
|
||||
manageable_role_names = set(manageable_role_names)
|
||||
return manageable_role_names
|
||||
|
||||
|
||||
class FakeOpenstackClient(object):
|
||||
class Quotas(object):
|
||||
@ -662,7 +686,7 @@ class FakeOctaviaClient(object):
|
||||
self.cache[project_id] = {
|
||||
name: [] for name in self.resource_dict.keys()}
|
||||
self.cache[project_id]['quota'] = dict(
|
||||
settings.PROJECT_QUOTA_SIZES['small']['octavia'])
|
||||
CONF.quota.sizes['small']['octavia'])
|
||||
|
||||
def __getattr__(self, name):
|
||||
# NOTE(amelia): This is out of pure laziness
|
||||
@ -748,7 +772,7 @@ def setup_neutron_cache(region, project_id):
|
||||
}
|
||||
|
||||
neutron_cache[region][project_id]['quota'] = dict(
|
||||
settings.PROJECT_QUOTA_SIZES['small']['neutron'])
|
||||
CONF.quota.sizes['small']['neutron'])
|
||||
|
||||
|
||||
def setup_cinder_cache(region, project_id):
|
||||
@ -764,7 +788,7 @@ def setup_cinder_cache(region, project_id):
|
||||
}
|
||||
|
||||
cinder_cache[region][project_id]['quota'] = dict(
|
||||
settings.PROJECT_QUOTA_SIZES['small']['cinder'])
|
||||
CONF.quota.sizes['small']['cinder'])
|
||||
|
||||
|
||||
def setup_nova_cache(region, project_id):
|
||||
@ -785,7 +809,7 @@ def setup_nova_cache(region, project_id):
|
||||
}
|
||||
}
|
||||
nova_cache[region][project_id]['quota'] = dict(
|
||||
settings.PROJECT_QUOTA_SIZES['small']['nova'])
|
||||
CONF.quota.sizes['small']['nova'])
|
||||
|
||||
|
||||
def setup_quota_cache(region_name, project_id, size='small'):
|
||||
@ -801,7 +825,7 @@ def setup_quota_cache(region_name, project_id, size='small'):
|
||||
}
|
||||
|
||||
cinder_cache[region_name][project_id]['quota'] = dict(
|
||||
settings.PROJECT_QUOTA_SIZES[size]['cinder'])
|
||||
CONF.quota.sizes[size]['cinder'])
|
||||
|
||||
global nova_cache
|
||||
if region_name not in nova_cache:
|
||||
@ -813,7 +837,7 @@ def setup_quota_cache(region_name, project_id, size='small'):
|
||||
}
|
||||
|
||||
nova_cache[region_name][project_id]['quota'] = dict(
|
||||
settings.PROJECT_QUOTA_SIZES[size]['nova'])
|
||||
CONF.quota.sizes[size]['nova'])
|
||||
|
||||
global neutron_cache
|
||||
if region_name not in neutron_cache:
|
||||
@ -825,7 +849,7 @@ def setup_quota_cache(region_name, project_id, size='small'):
|
||||
}
|
||||
|
||||
neutron_cache[region_name][project_id]['quota'] = dict(
|
||||
settings.PROJECT_QUOTA_SIZES[size]['neutron'])
|
||||
CONF.quota.sizes[size]['neutron'])
|
||||
|
||||
|
||||
def setup_mock_caches(region, project_id):
|
||||
|
@ -1,263 +0,0 @@
|
||||
# Copyright (C) 2017 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.
|
||||
|
||||
import mock
|
||||
|
||||
from rest_framework import status
|
||||
|
||||
from adjutant.api.models import Token
|
||||
from adjutant.common.tests import fake_clients
|
||||
from adjutant.common.tests.fake_clients import (
|
||||
FakeManager, setup_identity_cache)
|
||||
from adjutant.common.tests.utils import (AdjutantAPITestCase,
|
||||
modify_dict_settings)
|
||||
|
||||
from django.core import mail
|
||||
|
||||
|
||||
@mock.patch('adjutant.common.user_store.IdentityManager',
|
||||
FakeManager)
|
||||
class ModifySettingsTests(AdjutantAPITestCase):
|
||||
"""
|
||||
Tests designed to test the modify_dict_settings decorator.
|
||||
This is a bit weird to test because it's hard to directly test
|
||||
a lot of this stuff (especially in cases where dicts are updated rather
|
||||
than overridden).
|
||||
"""
|
||||
|
||||
# NOTE(amelia): Assumes the default settings for ResetUserPasswordAction
|
||||
# are that blacklisted roles are ['admin']
|
||||
|
||||
def test_modify_settings_override_password(self):
|
||||
"""
|
||||
Test override reset, by changing the reset password blacklisted roles
|
||||
"""
|
||||
|
||||
user = fake_clients.FakeUser(
|
||||
name="test@example.com", password="test_password",
|
||||
email="test@example.com")
|
||||
|
||||
user2 = fake_clients.FakeUser(
|
||||
name="admin@example.com", password="admin_password",
|
||||
email="admin@example.com")
|
||||
|
||||
project = fake_clients.FakeProject(name="test_project")
|
||||
|
||||
test_role = fake_clients.FakeRole("test_role")
|
||||
|
||||
assignments = [
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="test_role",
|
||||
user={'id': user.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="admin",
|
||||
user={'id': user2.id}
|
||||
),
|
||||
]
|
||||
|
||||
setup_identity_cache(
|
||||
projects=[project], users=[user, user2],
|
||||
role_assignments=assignments, extra_roles=[test_role])
|
||||
|
||||
url = "/v1/actions/ResetPassword"
|
||||
data = {'email': "test@example.com"}
|
||||
admin_data = {'email': 'admin@example.com'}
|
||||
|
||||
override = {
|
||||
'key_list': ['reset_user_password', 'action_settings',
|
||||
'ResetUserPasswordAction', 'blacklisted_roles'],
|
||||
'operation': 'override',
|
||||
'value': ['test_role']}
|
||||
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(1, Token.objects.count())
|
||||
|
||||
# NOTE(amelia): This next bit relies on the default settings being
|
||||
# that admins can't reset their own password
|
||||
with self.modify_dict_settings(TASK_SETTINGS=override):
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(0, Token.objects.count())
|
||||
|
||||
response2 = self.client.post(url, admin_data, format='json')
|
||||
self.assertEqual(response2.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(1, Token.objects.count())
|
||||
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(2, Token.objects.count())
|
||||
|
||||
response = self.client.post(url, admin_data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(1, Token.objects.count())
|
||||
|
||||
def test_modify_settings_remove_password(self):
|
||||
"""
|
||||
Test override reset, by changing the reset password blacklisted roles
|
||||
"""
|
||||
|
||||
user = fake_clients.FakeUser(
|
||||
name="admin@example.com", password="admin_password",
|
||||
email="admin@example.com")
|
||||
|
||||
project = fake_clients.FakeProject(name="test_project")
|
||||
|
||||
assignment = fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="admin",
|
||||
user={'id': user.id}
|
||||
)
|
||||
|
||||
setup_identity_cache(
|
||||
projects=[project], users=[user], role_assignments=[assignment])
|
||||
|
||||
url = "/v1/actions/ResetPassword"
|
||||
data = {'email': 'admin@example.com'}
|
||||
|
||||
override = {
|
||||
'key_list': ['reset_user_password', 'action_settings',
|
||||
'ResetUserPasswordAction', 'blacklisted_roles'],
|
||||
'operation': 'remove',
|
||||
'value': ['admin']}
|
||||
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(0, Token.objects.count())
|
||||
|
||||
with self.modify_dict_settings(TASK_SETTINGS=override):
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(1, Token.objects.count())
|
||||
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(0, Token.objects.count())
|
||||
|
||||
@modify_dict_settings(TASK_SETTINGS={
|
||||
'key_list': ['reset_user_password', 'action_settings',
|
||||
'ResetUserPasswordAction', 'blacklisted_roles'],
|
||||
'operation': 'append',
|
||||
'value': ['test_role']})
|
||||
def test_modify_settings_append_password(self):
|
||||
"""
|
||||
Test override reset, by changing the reset password blacklisted roles
|
||||
"""
|
||||
|
||||
user = fake_clients.FakeUser(
|
||||
name="test@example.com", password="test_password",
|
||||
email="test@example.com")
|
||||
|
||||
user2 = fake_clients.FakeUser(
|
||||
name="admin@example.com", password="admin_password",
|
||||
email="admin@example.com")
|
||||
|
||||
project = fake_clients.FakeProject(name="test_project")
|
||||
|
||||
test_role = fake_clients.FakeRole("test_role")
|
||||
|
||||
assignments = [
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="test_role",
|
||||
user={'id': user.id}
|
||||
),
|
||||
fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="admin",
|
||||
user={'id': user2.id}
|
||||
),
|
||||
]
|
||||
|
||||
setup_identity_cache(
|
||||
projects=[project], users=[user, user2],
|
||||
role_assignments=assignments, extra_roles=[test_role])
|
||||
|
||||
url = "/v1/actions/ResetPassword"
|
||||
data = {'email': "test@example.com"}
|
||||
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(0, Token.objects.count())
|
||||
|
||||
admin_data = {'email': 'admin@example.com'}
|
||||
response2 = self.client.post(url, admin_data, format='json')
|
||||
self.assertEqual(response2.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(0, Token.objects.count())
|
||||
|
||||
def test_modify_settings_update_email(self):
|
||||
"""
|
||||
Tests the update operator using email sending
|
||||
"""
|
||||
|
||||
user = fake_clients.FakeUser(
|
||||
name="test@example.com", password="test_password",
|
||||
email="test@example.com")
|
||||
|
||||
project = fake_clients.FakeProject(name="test_project")
|
||||
|
||||
assignment = fake_clients.FakeRoleAssignment(
|
||||
scope={'project': {'id': project.id}},
|
||||
role_name="project_admin",
|
||||
user={'id': user.id}
|
||||
)
|
||||
|
||||
setup_identity_cache(
|
||||
projects=[project], users=[user], role_assignments=[assignment])
|
||||
|
||||
url = "/v1/actions/UpdateEmail"
|
||||
data = {'new_email': "new_test@example.com"}
|
||||
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': project.id,
|
||||
'roles': "project_admin,_member_,project_mod",
|
||||
'username': "test@example.com",
|
||||
'user_id': user.id,
|
||||
'authenticated': True
|
||||
}
|
||||
|
||||
override = [
|
||||
{'key_list': ['update_user_email', 'emails', 'token'],
|
||||
'operation': 'update',
|
||||
'value': {
|
||||
'subject': 'modified_token_email',
|
||||
'template': 'update_user_email_token.txt'}
|
||||
}
|
||||
]
|
||||
|
||||
response = self.client.post(url, data, headers=headers, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertNotEqual(mail.outbox[0].subject, 'modified_token_email')
|
||||
|
||||
with self.modify_dict_settings(TASK_SETTINGS=override):
|
||||
data = {'new_email': "test2@example.com"}
|
||||
|
||||
response = self.client.post(url, data,
|
||||
headers=headers, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
self.assertEqual(mail.outbox[1].subject, 'modified_token_email')
|
||||
|
||||
data = {'new_email': "test3@example.com"}
|
||||
|
||||
response = self.client.post(url, data, headers=headers, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
|
||||
self.assertEqual(len(mail.outbox), 3)
|
||||
self.assertNotEqual(mail.outbox[2].subject, 'modified_token_email')
|
@ -12,194 +12,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import copy
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from adjutant.common.tests import fake_clients
|
||||
|
||||
|
||||
class modify_dict_settings(override_settings):
|
||||
"""
|
||||
A decorator like djangos modify_settings and override_settings, but makes
|
||||
it possible to do those same operations on dict based settings.
|
||||
|
||||
The decorator will act after both override_settings and modify_settings.
|
||||
|
||||
Can be applied to test functions or AdjutantTestCase,
|
||||
AdjutantAPITestCase classes. In those two classes settings can also
|
||||
be modified using:
|
||||
|
||||
with self.modify_dict_settings(...):
|
||||
# code
|
||||
|
||||
Example Usage:
|
||||
@modify_dict_settings(ROLES_MAPPING=[
|
||||
{'key_list': ['project_mod'],
|
||||
'operation': 'remove',
|
||||
'value': 'heat_stack_owner'},
|
||||
{'key_list': ['project_admin'],
|
||||
'operation': 'append',
|
||||
'value': 'heat_stack_owner'},
|
||||
])
|
||||
or
|
||||
@modify_dict_settings(PROJECT_QUOTA_SIZES={
|
||||
'key_list': ['small', 'nova', 'instances'],
|
||||
'operations': 'override',
|
||||
'value': 11
|
||||
})
|
||||
|
||||
Available operations:
|
||||
Standard operations:
|
||||
- 'update': A dict on dict operation to update final dict with value.
|
||||
- 'override': Either overrides or adds the value to the dictionary.
|
||||
- 'delete': Removes the value from the dictionary.
|
||||
|
||||
List operations:
|
||||
List operations expect that the accessed value in the dictionary is a list.
|
||||
- 'append': Add the specified values to the end of the list
|
||||
- 'prepend': Add the specifed values to the start of the list
|
||||
- 'remove': Remove the specified values from the list
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if args:
|
||||
# Hack used when instantiating from SimpleTestCase.setUpClass.
|
||||
assert not kwargs
|
||||
self.operations = args[0]
|
||||
else:
|
||||
assert not args
|
||||
self.operations = list(kwargs.items())
|
||||
super(override_settings, self).__init__()
|
||||
|
||||
def save_options(self, test_func):
|
||||
if getattr(test_func, "_modified_dict_settings", None) is None:
|
||||
test_func._modified_dict_settings = self.operations
|
||||
else:
|
||||
# Duplicate list to prevent subclasses from altering their parent.
|
||||
test_func._modified_dict_settings = list(
|
||||
test_func._modified_dict_settings) + self.operations
|
||||
|
||||
def disable(self):
|
||||
self.wrapped = self._wrapped
|
||||
for update_dict in self.update_dicts:
|
||||
update_dict['pointer'].clear()
|
||||
update_dict['pointer'].update(update_dict['copy'])
|
||||
|
||||
super(modify_dict_settings, self).disable()
|
||||
|
||||
def enable(self):
|
||||
self.options = {}
|
||||
|
||||
self.update_dicts = []
|
||||
self._wrapped = copy.deepcopy(settings._wrapped)
|
||||
|
||||
for name, operation_list in self.operations:
|
||||
try:
|
||||
value = self.options[name]
|
||||
except KeyError:
|
||||
value = getattr(settings, name, [])
|
||||
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("Initial setting not dictionary.")
|
||||
|
||||
if not isinstance(operation_list, list):
|
||||
operation_list = [operation_list]
|
||||
|
||||
for operation in operation_list:
|
||||
op_type = operation['operation']
|
||||
|
||||
holding_dict = value
|
||||
|
||||
# Recursively find the dict we want
|
||||
key_len = len(operation['key_list'])
|
||||
final_key = operation['key_list'][0]
|
||||
|
||||
for i in range(key_len):
|
||||
current_key = operation['key_list'][i]
|
||||
if i == (key_len - 1):
|
||||
final_key = current_key
|
||||
else:
|
||||
try:
|
||||
holding_dict = holding_dict[current_key]
|
||||
except KeyError:
|
||||
holding_dict[current_key] = {}
|
||||
holding_dict = holding_dict[current_key]
|
||||
|
||||
if op_type == "override":
|
||||
holding_dict[final_key] = operation['value']
|
||||
elif op_type == "delete":
|
||||
del holding_dict[final_key]
|
||||
elif op_type == "update":
|
||||
# Needs to be saved seperately and update re-used on
|
||||
# disable due to pointers
|
||||
self.update_dicts.append(
|
||||
{'pointer': holding_dict[final_key],
|
||||
'copy': copy.deepcopy(holding_dict[final_key])})
|
||||
holding_dict[final_key].update(operation['value'])
|
||||
else:
|
||||
val = holding_dict.get(final_key, [])
|
||||
items = operation['value']
|
||||
|
||||
if not isinstance(items, list):
|
||||
items = [items]
|
||||
|
||||
if op_type == 'append':
|
||||
holding_dict[final_key] = val + [
|
||||
item for item in items if item not in val]
|
||||
elif op_type == 'prepend':
|
||||
holding_dict[final_key] = ([item for item in items if
|
||||
item not in val] + val)
|
||||
elif op_type == 'remove':
|
||||
holding_dict[final_key] = [
|
||||
item for item in val if item not in items]
|
||||
else:
|
||||
raise ValueError("Unsupported action: %s" % op_type)
|
||||
self.options[name] = value
|
||||
super(modify_dict_settings, self).enable()
|
||||
|
||||
|
||||
class TestCaseMixin(object):
|
||||
""" Mixin to add modify_dict_settings functions to test classes """
|
||||
|
||||
@classmethod
|
||||
def _apply_settings_changes(cls):
|
||||
if getattr(cls, '_modified_dict_settings', None):
|
||||
operations = {}
|
||||
for key, value in cls._modified_dict_settings:
|
||||
operations[key] = value
|
||||
cls._cls_modified_dict_context = modify_dict_settings(
|
||||
**operations)
|
||||
cls._cls_modified_dict_context.enable()
|
||||
|
||||
@classmethod
|
||||
def _remove_settings_changes(cls):
|
||||
if hasattr(cls, '_cls_modified_dict_context'):
|
||||
cls._cls_modified_dict_context.disable()
|
||||
delattr(cls, '_cls_modified_dict_context')
|
||||
|
||||
def modify_dict_settings(self, **kwargs):
|
||||
return modify_dict_settings(**kwargs)
|
||||
|
||||
|
||||
class AdjutantTestCase(TestCase, TestCaseMixin):
|
||||
"""
|
||||
TestCase override that has support for @modify_dict_settings as a
|
||||
class decorator and internal function
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(AdjutantTestCase, cls).setUpClass()
|
||||
cls._apply_settings_changes()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls._remove_settings_changes()
|
||||
super(AdjutantTestCase, cls).tearDownClass()
|
||||
class AdjutantTestCase(TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
fake_clients.identity_cache.clear()
|
||||
@ -208,20 +27,7 @@ class AdjutantTestCase(TestCase, TestCaseMixin):
|
||||
fake_clients.cinder_cache.clear()
|
||||
|
||||
|
||||
class AdjutantAPITestCase(APITestCase, TestCaseMixin):
|
||||
"""
|
||||
APITestCase override that has support for @modify_dict_settings as a
|
||||
class decorator, and internal function
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(AdjutantAPITestCase, cls).setUpClass()
|
||||
cls._apply_settings_changes()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls._remove_settings_changes()
|
||||
super(AdjutantAPITestCase, cls).tearDownClass()
|
||||
class AdjutantAPITestCase(APITestCase):
|
||||
|
||||
def tearDown(self):
|
||||
fake_clients.identity_cache.clear()
|
||||
|
@ -14,32 +14,15 @@
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from keystoneclient import exceptions as ks_exceptions
|
||||
|
||||
from adjutant.config import CONF
|
||||
from adjutant.common.openstack_clients import get_keystoneclient
|
||||
|
||||
|
||||
def get_managable_roles(user_roles):
|
||||
"""
|
||||
Given a list of user role names, returns a list of names
|
||||
that the user is allowed to manage.
|
||||
"""
|
||||
manage_mapping = settings.ROLES_MAPPING
|
||||
# merge mapping lists to form a flat permitted roles list
|
||||
managable_role_names = [mrole for role_name in user_roles
|
||||
if role_name in manage_mapping
|
||||
for mrole in manage_mapping[role_name]]
|
||||
# a set has unique items
|
||||
managable_role_names = set(managable_role_names)
|
||||
return managable_role_names
|
||||
|
||||
|
||||
def subtree_ids_list(subtree, id_list=None):
|
||||
if id_list is None:
|
||||
id_list = []
|
||||
|
||||
if not subtree:
|
||||
return id_list
|
||||
for key in subtree.keys():
|
||||
@ -64,7 +47,7 @@ class IdentityManager(object): # pragma: no cover
|
||||
|
||||
# TODO(adriant): decide if we want to have some function calls
|
||||
# throw errors if this is false.
|
||||
self.can_edit_users = settings.KEYSTONE.get('can_edit_users', True)
|
||||
self.can_edit_users = CONF.identity.can_edit_users
|
||||
|
||||
def find_user(self, name, domain):
|
||||
try:
|
||||
@ -355,3 +338,28 @@ class IdentityManager(object): # pragma: no cover
|
||||
for cred in credentials:
|
||||
if cred.user_id == user_id and cred.type == cred_type:
|
||||
self.ks_client.credentials.delete(cred)
|
||||
|
||||
# TODO(adriant): Move this to a BaseIdentityManager class when
|
||||
# it exists.
|
||||
def get_manageable_roles(self, user_roles=None):
|
||||
"""Get roles which can be managed
|
||||
|
||||
Given a list of user role names, returns a list of names
|
||||
that the user is allowed to manage.
|
||||
|
||||
If user_roles is not given, returns all possible roles.
|
||||
"""
|
||||
roles_mapping = CONF.identity.role_mapping
|
||||
if user_roles is None:
|
||||
all_roles = []
|
||||
for options in roles_mapping.values():
|
||||
all_roles += options
|
||||
return list(set(all_roles))
|
||||
|
||||
# merge mapping lists to form a flat permitted roles list
|
||||
manageable_role_names = [mrole for role_name in user_roles
|
||||
if role_name in roles_mapping
|
||||
for mrole in roles_mapping[role_name]]
|
||||
# a set has unique items
|
||||
manageable_role_names = set(manageable_role_names)
|
||||
return manageable_role_names
|
||||
|
105
adjutant/config/__init__.py
Normal file
105
adjutant/config/__init__.py
Normal file
@ -0,0 +1,105 @@
|
||||
# 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 os
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
from confspirator import load
|
||||
from confspirator import groups
|
||||
|
||||
from adjutant.config import api
|
||||
from adjutant.config import django
|
||||
from adjutant.config import identity
|
||||
from adjutant.config import notification
|
||||
from adjutant.config import quota
|
||||
from adjutant.config import workflow
|
||||
|
||||
_root_config = groups.ConfigGroup("adjutant")
|
||||
_root_config.register_child_config(django.config_group)
|
||||
_root_config.register_child_config(identity.config_group)
|
||||
_root_config.register_child_config(api.config_group)
|
||||
_root_config.register_child_config(notification.config_group)
|
||||
_root_config.register_child_config(workflow.config_group)
|
||||
_root_config.register_child_config(quota.config_group)
|
||||
|
||||
_config_file = "/etc/adjutant/adjutant.yaml"
|
||||
_old_config_file = "/etc/adjutant/conf.yaml"
|
||||
|
||||
|
||||
_test_mode_commands = [
|
||||
# Adjutant commands:
|
||||
'exampleconfig',
|
||||
# Django commands:
|
||||
'check',
|
||||
'makemigrations',
|
||||
'squashmigrations',
|
||||
'test',
|
||||
'testserver',
|
||||
]
|
||||
|
||||
|
||||
def _load_config():
|
||||
if "adjutant-api" in sys.argv[0] and sys.argv[1] in _test_mode_commands:
|
||||
test_mode = True
|
||||
else:
|
||||
test_mode = False
|
||||
|
||||
config_file_locations = [_config_file, _old_config_file]
|
||||
|
||||
conf_file = os.environ.get("ADJUTANT_CONFIG_FILE", None)
|
||||
|
||||
if conf_file:
|
||||
config_file_locations.insert(0, conf_file)
|
||||
|
||||
conf_dict = None
|
||||
used_config_loc = None
|
||||
for conf_file_loc in config_file_locations:
|
||||
try:
|
||||
with open(conf_file_loc) as f:
|
||||
# NOTE(adriant): we print because we don't yet know
|
||||
# where to log to
|
||||
print("Loading config from '%s'" % conf_file_loc)
|
||||
conf_dict = yaml.load(f, Loader=yaml.FullLoader)
|
||||
used_config_loc = conf_file_loc
|
||||
break
|
||||
except IOError:
|
||||
if not test_mode:
|
||||
print(
|
||||
"Conf file not found at '%s', trying next possible location."
|
||||
% conf_file_loc
|
||||
)
|
||||
|
||||
if used_config_loc != conf_file and used_config_loc == _old_config_file and not test_mode:
|
||||
print(
|
||||
"DEPRECATED: Using the old default config location '%s' is deprecated "
|
||||
"in favor of '%s', or setting a config location via the environment "
|
||||
"variable 'ADJUTANT_CONFIG_FILE'." % (_old_config_file, _config_file)
|
||||
)
|
||||
|
||||
if conf_dict is None:
|
||||
if not test_mode:
|
||||
print(
|
||||
"No valid conf file not found, will rely on defaults and "
|
||||
"environment variables.\n"
|
||||
"Config should be placed at '%s' or a location defined via the "
|
||||
"environment variable 'ADJUTANT_CONFIG_FILE'." % _config_file
|
||||
)
|
||||
conf_dict = {}
|
||||
|
||||
conf_dict = {"adjutant": conf_dict}
|
||||
return load(_root_config, conf_dict, test_mode=test_mode)
|
||||
|
||||
|
||||
CONF = _load_config()
|
52
adjutant/config/api.py
Normal file
52
adjutant/config/api.py
Normal file
@ -0,0 +1,52 @@
|
||||
# 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 groups
|
||||
from confspirator import fields
|
||||
|
||||
|
||||
config_group = groups.ConfigGroup("api")
|
||||
|
||||
config_group.register_child_config(
|
||||
fields.ListConfig(
|
||||
"active_delegate_apis",
|
||||
help_text="List of Active Delegate APIs.",
|
||||
required=True,
|
||||
default=[
|
||||
'UserRoles',
|
||||
'UserDetail',
|
||||
'UserResetPassword',
|
||||
'UserList',
|
||||
'RoleList',
|
||||
],
|
||||
# NOTE(adriant): for testing purposes we include ALL default APIs
|
||||
test_default=[
|
||||
'UserRoles',
|
||||
'UserDetail',
|
||||
'UserResetPassword',
|
||||
'UserList',
|
||||
'RoleList',
|
||||
'SignUp',
|
||||
'UpdateProjectQuotas',
|
||||
'CreateProjectAndUser',
|
||||
'InviteUser',
|
||||
'ResetPassword',
|
||||
'EditUser',
|
||||
'UpdateEmail',
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
delegate_apis_group = groups.ConfigGroup("delegate_apis", lazy_load=True)
|
||||
config_group.register_child_config(delegate_apis_group)
|
121
adjutant/config/django.py
Normal file
121
adjutant/config/django.py
Normal file
@ -0,0 +1,121 @@
|
||||
# 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 groups
|
||||
from confspirator import fields
|
||||
|
||||
|
||||
config_group = groups.ConfigGroup("django")
|
||||
|
||||
config_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"secret_key",
|
||||
help_text="The Django secret key.",
|
||||
required=True,
|
||||
default="Do not ever use this awful secret in prod!!!!",
|
||||
secret=True,
|
||||
unsafe_default=True,
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.BoolConfig(
|
||||
"debug",
|
||||
help_text="Django debug mode is turned on.",
|
||||
default=False,
|
||||
unsafe_default=True,
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.ListConfig(
|
||||
"allowed_hosts",
|
||||
help_text="The Django allowed hosts",
|
||||
required=True,
|
||||
default=["*"],
|
||||
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",
|
||||
help_text="Django databases config.",
|
||||
default={
|
||||
"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "db.sqlite3"}
|
||||
},
|
||||
is_json=True,
|
||||
unsafe_default=True,
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.DictConfig(
|
||||
"logging",
|
||||
help_text="A full override of the Django logging config for more customised logging.",
|
||||
is_json=True,
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"log_file",
|
||||
help_text="The name and location of the Adjutant log file, "
|
||||
"superceded by 'adjutant.django.logging'.",
|
||||
default="adjutant.log",
|
||||
)
|
||||
)
|
||||
|
||||
_email_group = groups.ConfigGroup("email")
|
||||
_email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"email_backend",
|
||||
help_text="Django email backend to use.",
|
||||
default="django.core.mail.backends.console.EmailBackend",
|
||||
required=True,
|
||||
)
|
||||
)
|
||||
_email_group.register_child_config(
|
||||
fields.IntConfig("timeout", help_text="Email backend timeout.")
|
||||
)
|
||||
_email_group.register_child_config(
|
||||
fields.HostNameConfig("host", help_text="Email backend server location.")
|
||||
)
|
||||
_email_group.register_child_config(
|
||||
fields.PortConfig("port", help_text="Email backend server port.")
|
||||
)
|
||||
_email_group.register_child_config(
|
||||
fields.StrConfig("host_user", help_text="Email backend user.")
|
||||
)
|
||||
_email_group.register_child_config(
|
||||
fields.StrConfig("host_password", help_text="Email backend user password.")
|
||||
)
|
||||
_email_group.register_child_config(
|
||||
fields.BoolConfig(
|
||||
"use_tls",
|
||||
help_text="Whether to use TLS for email. Mutually exclusive with 'use_ssl'.",
|
||||
default=False,
|
||||
)
|
||||
)
|
||||
_email_group.register_child_config(
|
||||
fields.BoolConfig(
|
||||
"use_ssl",
|
||||
help_text="Whether to use SSL for email. Mutually exclusive with 'use_tls'.",
|
||||
default=False,
|
||||
)
|
||||
)
|
||||
|
||||
config_group.register_child_config(_email_group)
|
137
adjutant/config/identity.py
Normal file
137
adjutant/config/identity.py
Normal file
@ -0,0 +1,137 @@
|
||||
# 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 groups
|
||||
from confspirator import fields
|
||||
from confspirator import types
|
||||
|
||||
|
||||
config_group = groups.ConfigGroup("identity")
|
||||
|
||||
config_group.register_child_config(
|
||||
fields.IntConfig(
|
||||
"token_cache_time",
|
||||
help_text="Cache time for Keystone Tokens in the Keystone Middleware.",
|
||||
default=-1,
|
||||
required=True,
|
||||
required_for_tests=False,
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.BoolConfig(
|
||||
"can_edit_users",
|
||||
help_text="Is Adjutant allowed (or able) to edit users in Keystone.",
|
||||
default=True,
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.BoolConfig(
|
||||
"username_is_email",
|
||||
help_text="Should Adjutant assume and treat all usernames as emails.",
|
||||
default=True,
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.DictConfig(
|
||||
"role_mapping",
|
||||
help_text="A mapping from held role to roles it is allowed to manage.",
|
||||
value_type=types.List(),
|
||||
check_value_type=True,
|
||||
is_json=True,
|
||||
default={
|
||||
'admin': [
|
||||
'project_admin',
|
||||
'project_mod',
|
||||
'heat_stack_owner',
|
||||
'member',
|
||||
],
|
||||
'project_admin': [
|
||||
'project_admin',
|
||||
'project_mod',
|
||||
'heat_stack_owner',
|
||||
'member',
|
||||
],
|
||||
'project_mod': [
|
||||
'project_mod',
|
||||
'heat_stack_owner',
|
||||
'member',
|
||||
],
|
||||
},
|
||||
test_default={
|
||||
"admin": ["project_admin", "project_mod", "member", "heat_stack_owner"],
|
||||
"project_admin": [
|
||||
"project_mod",
|
||||
"member",
|
||||
"heat_stack_owner",
|
||||
"project_admin",
|
||||
],
|
||||
"project_mod": ["member", "heat_stack_owner", "project_mod"],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
_auth_group = groups.ConfigGroup("auth")
|
||||
_auth_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"username",
|
||||
help_text="Username for Adjutant Keystone admin user.",
|
||||
required=True,
|
||||
required_for_tests=False,
|
||||
)
|
||||
)
|
||||
_auth_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"password",
|
||||
help_text="Password for Adjutant Keystone admin user.",
|
||||
required=True,
|
||||
secret=True,
|
||||
required_for_tests=False,
|
||||
)
|
||||
)
|
||||
_auth_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"project_name",
|
||||
help_text="Project name for Adjutant Keystone admin user.",
|
||||
required=True,
|
||||
required_for_tests=False,
|
||||
)
|
||||
)
|
||||
_auth_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"project_domain_id",
|
||||
help_text="Project domain id for Adjutant Keystone admin user.",
|
||||
default="default",
|
||||
required=True,
|
||||
required_for_tests=False,
|
||||
)
|
||||
)
|
||||
_auth_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"user_domain_id",
|
||||
help_text="User domain id for Adjutant Keystone admin user.",
|
||||
default="default",
|
||||
required=True,
|
||||
required_for_tests=False,
|
||||
)
|
||||
)
|
||||
_auth_group.register_child_config(
|
||||
fields.URIConfig(
|
||||
"auth_url",
|
||||
help_text="Keystone auth url that Adjutant will use.",
|
||||
schemes=["https", "http"],
|
||||
required=True,
|
||||
required_for_tests=False,
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(_auth_group)
|
21
adjutant/config/notification.py
Normal file
21
adjutant/config/notification.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.
|
||||
|
||||
from confspirator import groups
|
||||
|
||||
|
||||
config_group = groups.ConfigGroup("notifications")
|
||||
|
||||
handler_defaults_group = groups.ConfigGroup("handler_defaults", lazy_load=True)
|
||||
config_group.register_child_config(handler_defaults_group)
|
18
adjutant/config/plugin.py
Normal file
18
adjutant/config/plugin.py
Normal file
@ -0,0 +1,18 @@
|
||||
# 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 groups
|
||||
|
||||
|
||||
config_group = groups.ConfigGroup("plugin")
|
160
adjutant/config/quota.py
Normal file
160
adjutant/config/quota.py
Normal file
@ -0,0 +1,160 @@
|
||||
# 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 groups
|
||||
from confspirator import fields
|
||||
from confspirator import types
|
||||
|
||||
|
||||
DEFAULT_QUOTA_SIZES = {
|
||||
'small': {
|
||||
'nova': {
|
||||
'instances': 10,
|
||||
'cores': 20,
|
||||
'ram': 65536,
|
||||
'floating_ips': 10,
|
||||
'fixed_ips': 0,
|
||||
'metadata_items': 128,
|
||||
'injected_files': 5,
|
||||
'injected_file_content_bytes': 10240,
|
||||
'key_pairs': 50,
|
||||
'security_groups': 20,
|
||||
'security_group_rules': 100,
|
||||
},
|
||||
'cinder': {
|
||||
'gigabytes': 5000,
|
||||
'snapshots': 50,
|
||||
'volumes': 20,
|
||||
},
|
||||
'neutron': {
|
||||
'floatingip': 10,
|
||||
'network': 3,
|
||||
'port': 50,
|
||||
'router': 3,
|
||||
'security_group': 20,
|
||||
'security_group_rule': 100,
|
||||
'subnet': 3,
|
||||
},
|
||||
"octavia": {
|
||||
'health_monitor': 5,
|
||||
"listener": 1,
|
||||
"load_balancer": 1,
|
||||
"member": 2,
|
||||
"pool": 1,
|
||||
},
|
||||
},
|
||||
"medium": {
|
||||
"cinder": {
|
||||
"gigabytes": 10000,
|
||||
"volumes": 100,
|
||||
"snapshots": 300
|
||||
},
|
||||
"nova": {
|
||||
"metadata_items": 128,
|
||||
"injected_file_content_bytes": 10240,
|
||||
"ram": 327680,
|
||||
"floating_ips": 25,
|
||||
"key_pairs": 50,
|
||||
"instances": 50,
|
||||
"security_group_rules": 400,
|
||||
"injected_files": 5,
|
||||
"cores": 100,
|
||||
"fixed_ips": 0,
|
||||
"security_groups": 50
|
||||
},
|
||||
"neutron": {
|
||||
"security_group_rule": 400,
|
||||
"subnet": 5,
|
||||
"network": 5,
|
||||
"floatingip": 25,
|
||||
"security_group": 50,
|
||||
"router": 5,
|
||||
"port": 250
|
||||
},
|
||||
"octavia": {
|
||||
'health_monitor': 50,
|
||||
"listener": 5,
|
||||
"load_balancer": 5,
|
||||
"member": 5,
|
||||
"pool": 5,
|
||||
},
|
||||
},
|
||||
"large": {
|
||||
"cinder": {
|
||||
"gigabytes": 50000,
|
||||
"volumes": 200,
|
||||
"snapshots": 600
|
||||
},
|
||||
"nova": {
|
||||
"metadata_items": 128,
|
||||
"injected_file_content_bytes": 10240,
|
||||
"ram": 655360,
|
||||
"floating_ips": 50,
|
||||
"key_pairs": 50,
|
||||
"instances": 100,
|
||||
"security_group_rules": 800,
|
||||
"injected_files": 5,
|
||||
"cores": 200,
|
||||
"fixed_ips": 0,
|
||||
"security_groups": 100
|
||||
},
|
||||
"neutron": {
|
||||
"security_group_rule": 800,
|
||||
"subnet": 10,
|
||||
"network": 10,
|
||||
"floatingip": 50,
|
||||
"security_group": 100,
|
||||
"router": 10,
|
||||
"port": 500
|
||||
},
|
||||
"octavia": {
|
||||
'health_monitor': 100,
|
||||
"listener": 10,
|
||||
"load_balancer": 10,
|
||||
"member": 10,
|
||||
"pool": 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
config_group = groups.ConfigGroup("quota")
|
||||
|
||||
config_group.register_child_config(
|
||||
fields.DictConfig(
|
||||
"sizes",
|
||||
help_text="A definition of the quota size groups that Adjutant should use.",
|
||||
value_type=types.Dict(value_type=types.Dict()),
|
||||
check_value_type=True,
|
||||
is_json=True,
|
||||
default=DEFAULT_QUOTA_SIZES,
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.ListConfig(
|
||||
"sizes_ascending",
|
||||
help_text="An ascending list of all the quota size names, "
|
||||
"so that Adjutant knows their relative sizes/order.",
|
||||
default=['small', 'medium', 'large'],
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.DictConfig(
|
||||
"services",
|
||||
help_text="A per region definition of what services Adjutant should manage "
|
||||
"quotas for. '*' means all or default region.",
|
||||
value_type=types.List(),
|
||||
default={'*': ['cinder', 'neutron', 'nova']},
|
||||
)
|
||||
)
|
170
adjutant/config/workflow.py
Normal file
170
adjutant/config/workflow.py
Normal file
@ -0,0 +1,170 @@
|
||||
# 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 groups
|
||||
from confspirator import fields
|
||||
|
||||
|
||||
config_group = groups.ConfigGroup("workflow")
|
||||
|
||||
config_group.register_child_config(
|
||||
fields.URIConfig(
|
||||
"horizon_url",
|
||||
help_text="The base Horizon url for Adjutant to use when producing links to Horizon.",
|
||||
schemes=["https", "http"],
|
||||
required=True,
|
||||
sample_default="http://localhost/",
|
||||
test_default="http://localhost/",
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.IntConfig(
|
||||
"default_token_expiry",
|
||||
help_text="The default token expiry time for Task tokens.",
|
||||
default=24 * 60 * 60, # 24hrs in seconds
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _build_default_email_group(
|
||||
group_name,
|
||||
email_subject,
|
||||
email_from,
|
||||
email_reply,
|
||||
email_template,
|
||||
email_html_template,
|
||||
):
|
||||
email_group = groups.ConfigGroup(group_name)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"subject",
|
||||
help_text="Default email subject for this stage",
|
||||
default=email_subject)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"from",
|
||||
help_text="Default from email for this stage",
|
||||
default=email_from)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"reply",
|
||||
help_text="Default reply-to email for this stage",
|
||||
default=email_reply)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"template",
|
||||
help_text="Default email template for this stage",
|
||||
default=email_template)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"html_template",
|
||||
help_text="Default email html template for this stage",
|
||||
default=email_html_template)
|
||||
)
|
||||
return email_group
|
||||
|
||||
|
||||
_task_defaults_group = groups.ConfigGroup("task_defaults")
|
||||
config_group.register_child_config(_task_defaults_group)
|
||||
|
||||
_email_defaults_group = groups.ConfigGroup("emails")
|
||||
_task_defaults_group.register_child_config(_email_defaults_group)
|
||||
_email_defaults_group.register_child_config(
|
||||
_build_default_email_group(
|
||||
group_name="initial",
|
||||
email_subject="Task Confirmation",
|
||||
email_reply="no-reply@example.com",
|
||||
email_from="bounce+%(task_uuid)s@example.com",
|
||||
email_template="initial.txt",
|
||||
email_html_template=None,
|
||||
)
|
||||
)
|
||||
_email_defaults_group.register_child_config(
|
||||
_build_default_email_group(
|
||||
group_name="token",
|
||||
email_subject="Task Token",
|
||||
email_reply="no-reply@example.com",
|
||||
email_from="bounce+%(task_uuid)s@example.com",
|
||||
email_template="token.txt",
|
||||
email_html_template=None,
|
||||
)
|
||||
)
|
||||
_email_defaults_group.register_child_config(
|
||||
_build_default_email_group(
|
||||
group_name="completed",
|
||||
email_subject="Task Completed",
|
||||
email_reply="no-reply@example.com",
|
||||
email_from="bounce+%(task_uuid)s@example.com",
|
||||
email_template="completed.txt",
|
||||
email_html_template=None,
|
||||
)
|
||||
)
|
||||
|
||||
_notifications_defaults_group = groups.ConfigGroup("notifications")
|
||||
_task_defaults_group.register_child_config(_notifications_defaults_group)
|
||||
|
||||
_notifications_defaults_group.register_child_config(
|
||||
fields.ListConfig(
|
||||
"standard_handlers",
|
||||
help_text="Handlers to use for standard notifications.",
|
||||
required=True,
|
||||
default=[
|
||||
'EmailNotification',
|
||||
],
|
||||
)
|
||||
)
|
||||
_notifications_defaults_group.register_child_config(
|
||||
fields.ListConfig(
|
||||
"error_handlers",
|
||||
help_text="Handlers to use for error notifications.",
|
||||
required=True,
|
||||
default=[
|
||||
'EmailNotification',
|
||||
],
|
||||
)
|
||||
)
|
||||
_notifications_defaults_group.register_child_config(
|
||||
fields.DictConfig(
|
||||
"standard_handler_config",
|
||||
help_text="Settings for standard notification handlers.",
|
||||
default={},
|
||||
is_json=True,
|
||||
)
|
||||
)
|
||||
_notifications_defaults_group.register_child_config(
|
||||
fields.DictConfig(
|
||||
"error_handler_config",
|
||||
help_text="Settings for error notification handlers.",
|
||||
default={},
|
||||
is_json=True,
|
||||
)
|
||||
)
|
||||
_notifications_defaults_group.register_child_config(
|
||||
fields.ListConfig(
|
||||
"safe_errors",
|
||||
help_text="Error types which are safe to acknowledge automatically.",
|
||||
required=True,
|
||||
default=['SMTPException'],
|
||||
)
|
||||
)
|
||||
|
||||
action_defaults_group = groups.ConfigGroup("action_defaults", lazy_load=True)
|
||||
tasks_group = groups.ConfigGroup("tasks", lazy_load=True)
|
||||
|
||||
config_group.register_child_config(action_defaults_group)
|
||||
config_group.register_child_config(tasks_group)
|
@ -0,0 +1,15 @@
|
||||
# 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.
|
||||
|
||||
NOTIFICATION_HANDLERS = {}
|
@ -15,20 +15,57 @@
|
||||
from logging import getLogger
|
||||
from smtplib import SMTPException
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template import loader
|
||||
from django.utils import timezone
|
||||
|
||||
from confspirator import groups
|
||||
from confspirator import fields
|
||||
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
|
||||
|
||||
|
||||
class NotificationEngine(object):
|
||||
class BaseNotificationHandler(object):
|
||||
""""""
|
||||
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
self.logger = getLogger('adjutant')
|
||||
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)
|
||||
@ -37,64 +74,77 @@ class NotificationEngine(object):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class EmailNotification(NotificationEngine):
|
||||
class EmailNotification(BaseNotificationHandler):
|
||||
"""
|
||||
Basic email notification engine. Will
|
||||
Basic email notification handler. Will
|
||||
send an email with the given templates.
|
||||
|
||||
Example conf:
|
||||
<task_type>:
|
||||
notifications:
|
||||
EmailNotification:
|
||||
standard:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
template: notification.txt
|
||||
html_template: completed.txt
|
||||
error:
|
||||
emails:
|
||||
- errors@example.com
|
||||
reply: no-reply@example.com
|
||||
template: notification.txt
|
||||
html_template: completed.txt
|
||||
<other notification engine>:
|
||||
...
|
||||
"""
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
"emails",
|
||||
help_text="List of email addresses to send this notification to.",
|
||||
item_type=types.String(regex=constants.EMAIL_REGEX),
|
||||
default=[],
|
||||
),
|
||||
fields.StrConfig(
|
||||
"from",
|
||||
help_text="From email for this notification.",
|
||||
regex=constants.EMAIL_WITH_TEMPLATE_REGEX,
|
||||
sample_default="bounce+%(task_uuid)s@example.com",
|
||||
),
|
||||
fields.StrConfig(
|
||||
"reply",
|
||||
help_text="Reply-to email for this notification.",
|
||||
regex=constants.EMAIL_REGEX,
|
||||
sample_default="no-reply@example.com",
|
||||
),
|
||||
fields.StrConfig(
|
||||
"template",
|
||||
help_text="Email template for this notification. "
|
||||
"No template will cause the email not to send.",
|
||||
default="notification.txt",
|
||||
),
|
||||
fields.StrConfig(
|
||||
"html_template",
|
||||
help_text="Email html template for this notification.",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def _notify(self, task, notification):
|
||||
if not self.conf or not self.conf['emails']:
|
||||
conf = self.config(task, notification)
|
||||
if not conf or not conf["emails"]:
|
||||
# Log that we did this!!
|
||||
note = (
|
||||
"Skipped sending notification for task: %s "
|
||||
"as notification engine conf is None, or no emails "
|
||||
"were configured." % task.uuid
|
||||
"Skipped sending notification for task: %s (%s) "
|
||||
"as notification handler conf is None, or no emails "
|
||||
"were configured." % (task.task_type, task.uuid)
|
||||
)
|
||||
self.logger.info("(%s) - %s" % (timezone.now(), note))
|
||||
return
|
||||
|
||||
template = loader.get_template(
|
||||
self.conf['template'],
|
||||
using='include_etc_templates')
|
||||
html_template = self.conf.get('html_template', None)
|
||||
template = loader.get_template(conf["template"], using="include_etc_templates")
|
||||
html_template = conf["html_template"]
|
||||
if html_template:
|
||||
html_template = loader.get_template(
|
||||
html_template,
|
||||
using='include_etc_templates')
|
||||
html_template, using="include_etc_templates"
|
||||
)
|
||||
|
||||
context = {
|
||||
'task': task, 'notification': notification}
|
||||
context = {"task": task, "notification": notification}
|
||||
|
||||
if settings.HORIZON_URL:
|
||||
task_url = settings.HORIZON_URL
|
||||
notification_url = settings.HORIZON_URL
|
||||
if not task_url.endswith('/'):
|
||||
task_url += '/'
|
||||
task_url += 'management/tasks/%s' % task.uuid
|
||||
notification_url += (
|
||||
'management/notifications/%s' % notification.uuid)
|
||||
context['task_url'] = task_url
|
||||
context['notification_url'] = notification_url
|
||||
if CONF.workflow.horizon_url:
|
||||
task_url = CONF.workflow.horizon_url
|
||||
notification_url = CONF.workflow.horizon_url
|
||||
if not task_url.endswith("/"):
|
||||
task_url += "/"
|
||||
if not notification_url.endswith("/"):
|
||||
notification_url += "/"
|
||||
task_url += "management/tasks/%s" % task.uuid
|
||||
notification_url += "management/notifications/%s" % notification.uuid
|
||||
context["task_url"] = task_url
|
||||
context["notification_url"] = notification_url
|
||||
|
||||
if notification.error:
|
||||
subject = "Error - %s notification" % task.task_type
|
||||
@ -105,52 +155,54 @@ class EmailNotification(NotificationEngine):
|
||||
|
||||
# from_email is the return-path and is distinct from the
|
||||
# message headers
|
||||
from_email = self.conf.get('from')
|
||||
from_email = conf["from"]
|
||||
if not from_email:
|
||||
from_email = self.conf['reply']
|
||||
from_email = conf["reply"]
|
||||
elif "%(task_uuid)s" in from_email:
|
||||
from_email = from_email % {'task_uuid': task.uuid}
|
||||
from_email = from_email % {"task_uuid": task.uuid}
|
||||
|
||||
# these are the message headers which will be visible to
|
||||
# the email client.
|
||||
headers = {
|
||||
'X-Adjutant-Task-UUID': task.uuid,
|
||||
"X-Adjutant-Task-UUID": task.uuid,
|
||||
# From needs to be set to be disctinct from return-path
|
||||
'From': self.conf['reply'],
|
||||
'Reply-To': self.conf['reply'],
|
||||
"From": conf["reply"],
|
||||
"Reply-To": conf["reply"],
|
||||
}
|
||||
|
||||
email = EmailMultiAlternatives(
|
||||
subject,
|
||||
message,
|
||||
from_email,
|
||||
self.conf['emails'],
|
||||
headers=headers,
|
||||
subject, message, from_email, conf["emails"], headers=headers
|
||||
)
|
||||
|
||||
if html_template:
|
||||
email.attach_alternative(
|
||||
html_template.render(context), "text/html")
|
||||
email.attach_alternative(html_template.render(context), "text/html")
|
||||
|
||||
email.send(fail_silently=False)
|
||||
if not notification.error:
|
||||
notification.acknowledged = True
|
||||
notification.save()
|
||||
notification.acknowledged = True
|
||||
notification.save()
|
||||
except SMTPException as e:
|
||||
notes = {
|
||||
'errors':
|
||||
[("Error: '%s' while sending email notification") % e]
|
||||
}
|
||||
notes = {"errors": [("Error: '%s' while sending email notification") % e]}
|
||||
error_notification = Notification.objects.create(
|
||||
task=notification.task,
|
||||
notes=notes,
|
||||
error=True
|
||||
task=notification.task, notes=notes, error=True
|
||||
)
|
||||
error_notification.save()
|
||||
|
||||
|
||||
notification_engines = {
|
||||
'EmailNotification': EmailNotification,
|
||||
}
|
||||
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)
|
||||
|
||||
settings.NOTIFICATION_ENGINES.update(notification_engines)
|
||||
|
||||
register_notification_handler(EmailNotification)
|
||||
|
@ -18,64 +18,93 @@ from django.core import mail
|
||||
|
||||
from rest_framework import status
|
||||
|
||||
from confspirator.tests import utils as conf_utils
|
||||
|
||||
from adjutant.api.models import Task, Notification
|
||||
from adjutant.common.tests.fake_clients import (
|
||||
FakeManager, setup_identity_cache)
|
||||
from adjutant.common.tests.utils import (
|
||||
AdjutantAPITestCase, modify_dict_settings)
|
||||
from adjutant.common.tests.utils import AdjutantAPITestCase
|
||||
from adjutant.config import CONF
|
||||
from adjutant import exceptions
|
||||
|
||||
|
||||
@mock.patch('adjutant.common.user_store.IdentityManager',
|
||||
FakeManager)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.create_project_and_user.notifications": [
|
||||
{'operation': 'override', 'value': {
|
||||
"standard_handlers": ["EmailNotification"],
|
||||
"error_handlers": ["EmailNotification"],
|
||||
"standard_handler_config": {
|
||||
"EmailNotification": {
|
||||
'emails': ['example_notification@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
}
|
||||
},
|
||||
"error_handler_config": {
|
||||
"EmailNotification": {
|
||||
'emails': ['example_error_notification@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
}
|
||||
},
|
||||
}},
|
||||
],
|
||||
})
|
||||
class NotificationTests(AdjutantAPITestCase):
|
||||
|
||||
@modify_dict_settings(TASK_SETTINGS={
|
||||
'key_list': ['create_project', 'notifications'],
|
||||
'operation': 'override',
|
||||
'value': {
|
||||
'EmailNotification': {
|
||||
'standard': {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
'template': 'notification.txt'
|
||||
},
|
||||
'error': {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
'template': 'notification.txt'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
def test_new_project_sends_notification(self):
|
||||
"""
|
||||
Confirm that the email notification engine correctly acknowledges
|
||||
Confirm that the email notification handler correctly acknowledges
|
||||
notifications it sends out.
|
||||
"""
|
||||
|
||||
This tests standard and error notifications.
|
||||
"""
|
||||
setup_identity_cache()
|
||||
|
||||
url = "/v1/actions/CreateProjectAndUser"
|
||||
url = "/v1/openstack/sign-up"
|
||||
data = {'project_name': "test_project", 'email': "test@example.com"}
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
|
||||
new_task = Task.objects.all()[0]
|
||||
self.assertEqual(Notification.objects.count(), 1)
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
self.assertEqual(mail.outbox[1].subject, "create_project_and_user notification")
|
||||
self.assertEqual(mail.outbox[1].to, ['example_notification@example.com'])
|
||||
|
||||
notif = Notification.objects.all()[0]
|
||||
self.assertEqual(notif.task.uuid, new_task.uuid)
|
||||
self.assertFalse(notif.error)
|
||||
self.assertTrue(notif.acknowledged)
|
||||
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,_member_",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
}
|
||||
new_task = Task.objects.all()[0]
|
||||
url = "/v1/tasks/" + new_task.uuid
|
||||
response = self.client.post(url, {'approved': True}, format='json',
|
||||
headers=headers)
|
||||
with mock.patch(
|
||||
"adjutant.common.tests.fake_clients.FakeManager.find_project"
|
||||
) as mocked_find:
|
||||
mocked_find.side_effect = exceptions.ServiceUnavailable(
|
||||
"Forced key error for testing."
|
||||
)
|
||||
response = self.client.post(
|
||||
url, {"approved": True}, format="json", headers=headers
|
||||
)
|
||||
|
||||
self.assertEqual(Notification.objects.count(), 1)
|
||||
# should send token email, but no new notification
|
||||
self.assertEqual(Notification.objects.count(), 2)
|
||||
self.assertEqual(len(mail.outbox), 3)
|
||||
self.assertEqual(mail.outbox[2].subject, "Error - create_project_and_user notification")
|
||||
self.assertEqual(mail.outbox[2].to, ['example_error_notification@example.com'])
|
||||
|
||||
notif = Notification.objects.all()[0]
|
||||
notif = Notification.objects.all()[1]
|
||||
self.assertEqual(notif.task.uuid, new_task.uuid)
|
||||
self.assertTrue(notif.error)
|
||||
self.assertTrue(notif.acknowledged)
|
||||
|
42
adjutant/notifications/utils.py
Normal file
42
adjutant/notifications/utils.py
Normal file
@ -0,0 +1,42 @@
|
||||
# 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 notifications
|
||||
from adjutant.api.models import Notification
|
||||
|
||||
|
||||
def create_notification(task, notes, error=False, handlers=True):
|
||||
notification = Notification.objects.create(
|
||||
task=task,
|
||||
notes=notes,
|
||||
error=error
|
||||
)
|
||||
notification.save()
|
||||
|
||||
if not handlers:
|
||||
return notification
|
||||
|
||||
notif_conf = task.config.notifications
|
||||
|
||||
if error:
|
||||
notif_handlers = notif_conf.error_handlers
|
||||
else:
|
||||
notif_handlers = notif_conf.standard_handlers
|
||||
|
||||
if notif_handlers:
|
||||
for notif_handler in notif_handlers:
|
||||
handler = notifications.NOTIFICATION_HANDLERS[notif_handler]()
|
||||
handler.notify(task, notification)
|
||||
|
||||
return notification
|
46
adjutant/plugins.py
Normal file
46
adjutant/plugins.py
Normal file
@ -0,0 +1,46 @@
|
||||
# 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)
|
@ -25,9 +25,9 @@ https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
from adjutant.utils import setup_task_settings
|
||||
from adjutant.exceptions import ConfigurationException
|
||||
|
||||
from adjutant.config import CONF as adj_conf
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
# Application definition
|
||||
@ -41,10 +41,17 @@ INSTALLED_APPS = (
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework_swagger',
|
||||
|
||||
'adjutant.commands',
|
||||
'adjutant.actions',
|
||||
'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',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
@ -106,114 +113,63 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSION_CLASSES': [],
|
||||
}
|
||||
|
||||
# Setup of local settings data
|
||||
if 'test' in sys.argv:
|
||||
from adjutant import test_settings
|
||||
CONFIG = test_settings.conf_dict
|
||||
else:
|
||||
config_file = "/etc/adjutant/conf.yaml"
|
||||
if not os.path.isfile(config_file):
|
||||
print("%s does not exist. Reverting to default config file." %
|
||||
config_file)
|
||||
config_file = "conf/conf.yaml"
|
||||
with open(config_file) as f:
|
||||
CONFIG = yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
SECRET_KEY = CONFIG['SECRET_KEY']
|
||||
SECRET_KEY = adj_conf.django.secret_key
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = CONFIG.get('DEBUG', False)
|
||||
|
||||
DEBUG = adj_conf.django.debug
|
||||
if DEBUG:
|
||||
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append(
|
||||
'rest_framework.renderers.BrowsableAPIRenderer')
|
||||
|
||||
ALLOWED_HOSTS = CONFIG.get('ALLOWED_HOSTS', [])
|
||||
|
||||
for app in CONFIG['ADDITIONAL_APPS']:
|
||||
INSTALLED_APPS = list(INSTALLED_APPS)
|
||||
INSTALLED_APPS.append(app)
|
||||
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.append("adjutant.startup")
|
||||
INSTALLED_APPS = _INSTALLED_APPS
|
||||
|
||||
DATABASES = CONFIG['DATABASES']
|
||||
DATABASES = adj_conf.django.databases
|
||||
|
||||
LOGGING = CONFIG['LOGGING']
|
||||
if adj_conf.django.logging:
|
||||
LOGGING = adj_conf.django.logging
|
||||
else:
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'file': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': adj_conf.django.log_file,
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'adjutant': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'django': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'keystonemiddleware': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
EMAIL_BACKEND = CONFIG['EMAIL_SETTINGS']['EMAIL_BACKEND']
|
||||
EMAIL_TIMEOUT = 60
|
||||
EMAIL_BACKEND = adj_conf.django.email.email_backend
|
||||
EMAIL_TIMEOUT = adj_conf.django.email.timeout
|
||||
|
||||
EMAIL_HOST = CONFIG['EMAIL_SETTINGS'].get('EMAIL_HOST')
|
||||
EMAIL_PORT = CONFIG['EMAIL_SETTINGS'].get('EMAIL_PORT')
|
||||
EMAIL_HOST_USER = CONFIG['EMAIL_SETTINGS'].get('EMAIL_HOST_USER')
|
||||
EMAIL_HOST_PASSWORD = CONFIG['EMAIL_SETTINGS'].get('EMAIL_HOST_PASSWORD')
|
||||
EMAIL_USE_TLS = CONFIG['EMAIL_SETTINGS'].get('EMAIL_USE_TLS', False)
|
||||
EMAIL_USE_SSL = CONFIG['EMAIL_SETTINGS'].get('EMAIL_USE_SSL', False)
|
||||
|
||||
# setting to control if user name and email are allowed
|
||||
# to have different values.
|
||||
USERNAME_IS_EMAIL = CONFIG['USERNAME_IS_EMAIL']
|
||||
|
||||
# Keystone admin credentials:
|
||||
KEYSTONE = CONFIG['KEYSTONE']
|
||||
|
||||
TOKEN_SUBMISSION_URL = CONFIG.get('TOKEN_SUBMISSION_URL')
|
||||
if TOKEN_SUBMISSION_URL:
|
||||
print("'TOKEN_SUBMISSION_URL' is deprecated, use 'HORIZON_URL' instead")
|
||||
|
||||
HORIZON_URL = CONFIG.get('HORIZON_URL')
|
||||
|
||||
if not HORIZON_URL and not TOKEN_SUBMISSION_URL:
|
||||
raise ConfigurationException("Must supply 'HORIZON_URL'")
|
||||
|
||||
TOKEN_EXPIRE_TIME = CONFIG['TOKEN_EXPIRE_TIME']
|
||||
|
||||
DEFAULT_ACTION_SETTINGS = CONFIG['DEFAULT_ACTION_SETTINGS']
|
||||
|
||||
TASK_SETTINGS = setup_task_settings(
|
||||
CONFIG['DEFAULT_TASK_SETTINGS'],
|
||||
CONFIG['DEFAULT_ACTION_SETTINGS'],
|
||||
CONFIG['TASK_SETTINGS'])
|
||||
|
||||
DEFAULT_TASK_SETTINGS = CONFIG['DEFAULT_TASK_SETTINGS']
|
||||
|
||||
PLUGIN_SETTINGS = CONFIG.get('PLUGIN_SETTINGS', {})
|
||||
|
||||
ROLES_MAPPING = CONFIG['ROLES_MAPPING']
|
||||
|
||||
TOKEN_CACHE_TIME = CONFIG.get('TOKEN_CACHE_TIME', 60)
|
||||
|
||||
PROJECT_QUOTA_SIZES = CONFIG.get('PROJECT_QUOTA_SIZES')
|
||||
|
||||
QUOTA_SIZES_ASC = CONFIG.get('QUOTA_SIZES_ASC', [])
|
||||
|
||||
ACTIVE_DELEGATE_APIS = CONFIG.get(
|
||||
'ACTIVE_DELEGATE_APIS',
|
||||
[
|
||||
'UserRoles',
|
||||
'UserDetail',
|
||||
'UserResetPassword',
|
||||
'UserList',
|
||||
'RoleList'
|
||||
])
|
||||
|
||||
# Default services for which to check and update quotas for
|
||||
QUOTA_SERVICES = CONFIG.get(
|
||||
'QUOTA_SERVICES',
|
||||
{'*': ['cinder', 'neutron', 'nova']})
|
||||
|
||||
|
||||
# Dict of DelegateAPIs and their url_paths.
|
||||
# - This is populated by registering DelegateAPIs.
|
||||
DELEGATE_API_CLASSES = {}
|
||||
|
||||
# Dict of actions and their serializers.
|
||||
# - This is populated from the various model modules at startup:
|
||||
ACTION_CLASSES = {}
|
||||
|
||||
TASK_CLASSES = {}
|
||||
|
||||
NOTIFICATION_ENGINES = {}
|
||||
EMAIL_HOST = adj_conf.django.email.host
|
||||
EMAIL_PORT = adj_conf.django.email.port
|
||||
EMAIL_HOST_USER = adj_conf.django.email.host_user
|
||||
EMAIL_HOST_PASSWORD = adj_conf.django.email.host_password
|
||||
EMAIL_USE_TLS = adj_conf.django.email.use_tls
|
||||
EMAIL_USE_SSL = adj_conf.django.email.use_ssl
|
||||
|
@ -1,13 +1,14 @@
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
from adjutant.config import CONF
|
||||
from adjutant import actions, api, tasks
|
||||
from adjutant.exceptions import ActionNotRegistered, DelegateAPINotRegistered
|
||||
|
||||
|
||||
def check_expected_delegate_apis():
|
||||
missing_delegate_apis = list(
|
||||
set(settings.ACTIVE_DELEGATE_APIS)
|
||||
- set(settings.DELEGATE_API_CLASSES.keys()))
|
||||
set(CONF.api.active_delegate_apis)
|
||||
- set(api.DELEGATE_API_CLASSES.keys()))
|
||||
|
||||
if missing_delegate_apis:
|
||||
raise DelegateAPINotRegistered(
|
||||
@ -20,15 +21,15 @@ def check_configured_actions():
|
||||
"""Check that all the expected actions have been registered."""
|
||||
configured_actions = []
|
||||
|
||||
for task in settings.TASK_CLASSES:
|
||||
task_class = settings.TASK_CLASSES.get(task)
|
||||
for task in tasks.TASK_CLASSES:
|
||||
task_class = tasks.TASK_CLASSES.get(task)
|
||||
|
||||
configured_actions += task_class.default_actions
|
||||
configured_actions += settings.TASK_SETTINGS.get(
|
||||
task_class.task_type, {}).get('additional_actions', [])
|
||||
configured_actions += CONF.workflow.tasks.get(
|
||||
task_class.task_type).additional_actions
|
||||
|
||||
missing_actions = list(
|
||||
set(configured_actions) - set(settings.ACTION_CLASSES.keys()))
|
||||
set(configured_actions) - set(actions.ACTION_CLASSES.keys()))
|
||||
|
||||
if missing_actions:
|
||||
raise ActionNotRegistered(
|
||||
|
@ -0,0 +1,15 @@
|
||||
# Copyright (C) 2019 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.
|
||||
|
||||
TASK_CLASSES = {}
|
@ -12,12 +12,14 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from uuid import uuid4
|
||||
from django.utils import timezone
|
||||
from jsonfield import JSONField
|
||||
|
||||
from adjutant.config import CONF
|
||||
from adjutant import tasks
|
||||
|
||||
|
||||
def hex_uuid():
|
||||
return uuid4().hex
|
||||
@ -75,7 +77,15 @@ class Task(models.Model):
|
||||
|
||||
def get_task(self):
|
||||
"""Returns self as the appropriate task wrapper type."""
|
||||
return settings.TASK_CLASSES[self.task_type](task_model=self)
|
||||
return tasks.TASK_CLASSES[self.task_type](task_model=self)
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
try:
|
||||
task_conf = CONF.workflow.tasks[self.task_type]
|
||||
except KeyError:
|
||||
task_conf = {}
|
||||
return CONF.workflow.task_defaults.overlay(task_conf)
|
||||
|
||||
@property
|
||||
def actions(self):
|
||||
|
@ -15,16 +15,103 @@
|
||||
import hashlib
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from confspirator import groups
|
||||
from confspirator import fields
|
||||
|
||||
from adjutant import actions as adj_actions
|
||||
from adjutant.api.models import Task
|
||||
from adjutant.config import CONF
|
||||
from django.utils import timezone
|
||||
from adjutant.api.v1.utils import create_notification
|
||||
from adjutant.notifications.utils import create_notification
|
||||
from adjutant.tasks.v1.utils import (
|
||||
send_stage_email, create_token, handle_task_error)
|
||||
from adjutant import exceptions
|
||||
|
||||
|
||||
def make_task_config(task_class):
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup()
|
||||
config_group.register_child_config(
|
||||
fields.BoolConfig(
|
||||
"allow_auto_approve",
|
||||
help_text="Override if this task allows auto_approval. "
|
||||
"Otherwise uses task default.",
|
||||
default=task_class.allow_auto_approve,
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.ListConfig(
|
||||
"additional_actions",
|
||||
help_text="Additional actions to be run as part of the task "
|
||||
"after default actions.",
|
||||
default=task_class.additional_actions or [],
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.IntConfig(
|
||||
"token_expiry",
|
||||
help_text="Override for the task token expiry. "
|
||||
"Otherwise uses task default.",
|
||||
default=task_class.token_expiry,
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.DictConfig(
|
||||
"actions",
|
||||
help_text="Action config overrides over the action defaults. "
|
||||
"See 'adjutant.workflow.action_defaults'.",
|
||||
is_json=True,
|
||||
default=task_class.action_config or {},
|
||||
sample_default={
|
||||
"SomeCustomAction": {
|
||||
"some_action_setting": "<a-uuid-probably>"
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.DictConfig(
|
||||
"emails",
|
||||
help_text="Email config overrides for this task over task defaults."
|
||||
"See 'adjutant.workflow.emails'.",
|
||||
is_json=True,
|
||||
default=task_class.email_config or {},
|
||||
sample_default={
|
||||
"initial": None,
|
||||
"token": {
|
||||
"subject": "Some custom subject",
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.DictConfig(
|
||||
"notifications",
|
||||
help_text="Notification config overrides for this task over task defaults."
|
||||
"See 'adjutant.workflow.notifications'.",
|
||||
is_json=True,
|
||||
default=task_class.notification_config or {},
|
||||
sample_default={
|
||||
"standard_handlers": ["EmailNotification"],
|
||||
"error_handlers": ["EmailNotification"],
|
||||
"standard_handler_config": {
|
||||
"EmailNotification": {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
}
|
||||
},
|
||||
"error_handler_config": {
|
||||
"EmailNotification": {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
return config_group
|
||||
|
||||
|
||||
class BaseTask(object):
|
||||
"""
|
||||
Base class for in memory task representation.
|
||||
@ -37,23 +124,28 @@ class BaseTask(object):
|
||||
logic here, and includes some wrapper logic to help deal with workflows.
|
||||
"""
|
||||
|
||||
# default values to optionally override
|
||||
duplicate_policy = "cancel"
|
||||
allow_auto_approve = True
|
||||
send_approval_notification = True
|
||||
|
||||
# required values in custom task
|
||||
task_type = None
|
||||
default_actions = None
|
||||
|
||||
# optional values
|
||||
# default values to optionally override in task definition
|
||||
deprecated_task_types = None
|
||||
duplicate_policy = "cancel"
|
||||
send_approval_notification = True
|
||||
|
||||
# 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
|
||||
|
||||
def __init__(self,
|
||||
task_model=None,
|
||||
task_data=None,
|
||||
action_data=None):
|
||||
|
||||
self._config = None
|
||||
self.logger = getLogger('adjutant')
|
||||
|
||||
if task_model:
|
||||
@ -99,7 +191,7 @@ class BaseTask(object):
|
||||
actions = self.actions
|
||||
else:
|
||||
actions = self.default_actions[:]
|
||||
actions += self.settings.get('additional_actions', [])
|
||||
actions += self.config.additional_actions
|
||||
|
||||
# instantiate all action serializers and check validity
|
||||
valid = True
|
||||
@ -110,7 +202,7 @@ class BaseTask(object):
|
||||
action_name = action
|
||||
|
||||
action_class, serializer_class = \
|
||||
settings.ACTION_CLASSES[action_name]
|
||||
adj_actions.ACTION_CLASSES[action_name]
|
||||
|
||||
if use_existing_actions:
|
||||
action_class = action
|
||||
@ -152,7 +244,7 @@ class BaseTask(object):
|
||||
hashable_list.append(
|
||||
action['serializer'].validated_data[field])
|
||||
except KeyError:
|
||||
if field == "username" and settings.USERNAME_IS_EMAIL:
|
||||
if field == "username" and CONF.identity.username_is_email:
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
@ -188,12 +280,13 @@ class BaseTask(object):
|
||||
|
||||
def _create_token(self):
|
||||
self.clear_tokens()
|
||||
token = create_token(self.task)
|
||||
token_expiry = self.config.token_expiry or self.token_expiry
|
||||
token = create_token(self.task, token_expiry)
|
||||
self.add_note("Token created for task.")
|
||||
try:
|
||||
# will throw a key error if the token template has not
|
||||
# been specified
|
||||
email_conf = self.settings['emails']['token']
|
||||
email_conf = self.config.emails.token
|
||||
send_stage_email(self.task, email_conf, token)
|
||||
except KeyError as e:
|
||||
handle_task_error(e, self.task, error_text='while sending token')
|
||||
@ -209,15 +302,18 @@ class BaseTask(object):
|
||||
self.task.add_task_note(note)
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
"""Get my settings.
|
||||
def config(self):
|
||||
"""Get my config.
|
||||
|
||||
Returns a dict of the settings for this task.
|
||||
Returns a dict of the config for this task.
|
||||
"""
|
||||
try:
|
||||
return settings.TASK_SETTINGS[self.task_type]
|
||||
except KeyError:
|
||||
return settings.DEFAULT_TASK_SETTINGS
|
||||
if self._config is None:
|
||||
try:
|
||||
task_conf = CONF.workflow.tasks[self.task_type]
|
||||
except KeyError:
|
||||
task_conf = {}
|
||||
self._config = CONF.workflow.task_defaults.overlay(task_conf)
|
||||
return self._config
|
||||
|
||||
def is_valid(self, internal_message=None):
|
||||
self._refresh_actions()
|
||||
@ -301,7 +397,7 @@ class BaseTask(object):
|
||||
e, self.task, error_text='while setting up task')
|
||||
|
||||
# send initial confirmation email:
|
||||
email_conf = self.settings.get('emails', {}).get('initial', None)
|
||||
email_conf = self.config.emails.initial
|
||||
send_stage_email(self.task, email_conf)
|
||||
|
||||
approve_list = [act.auto_approve for act in self.actions]
|
||||
@ -316,8 +412,8 @@ class BaseTask(object):
|
||||
else:
|
||||
can_auto_approve = False
|
||||
|
||||
if self.settings.get('allow_auto_approve') is not None:
|
||||
allow_auto_approve = self.settings.get('allow_auto_approve')
|
||||
if self.config.allow_auto_approve is not None:
|
||||
allow_auto_approve = self.config.allow_auto_approve
|
||||
else:
|
||||
allow_auto_approve = self.allow_auto_approve
|
||||
|
||||
@ -427,8 +523,7 @@ class BaseTask(object):
|
||||
token.delete()
|
||||
|
||||
# Sending confirmation email:
|
||||
email_conf = self.settings.get(
|
||||
'emails', {}).get('completed', None)
|
||||
email_conf = self.config.emails.completed
|
||||
send_stage_email(self.task, email_conf)
|
||||
|
||||
def cancel(self):
|
||||
|
@ -16,9 +16,8 @@ from logging import getLogger
|
||||
|
||||
from six import string_types
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from adjutant import exceptions
|
||||
from adjutant import tasks
|
||||
from adjutant.tasks.models import Task
|
||||
from adjutant.tasks.v1.base import BaseTask
|
||||
|
||||
@ -35,9 +34,9 @@ class TaskManager(object):
|
||||
otherwise if it is a valid task class, will return it.
|
||||
"""
|
||||
try:
|
||||
return settings.TASK_CLASSES[task_type]
|
||||
return tasks.TASK_CLASSES[task_type]
|
||||
except KeyError:
|
||||
if task_type in settings.TASK_CLASSES.values():
|
||||
if task_type in tasks.TASK_CLASSES.values():
|
||||
return task_type
|
||||
raise exceptions.TaskNotRegistered(
|
||||
"Unknown task type: '%s'" % task_type)
|
||||
@ -69,7 +68,7 @@ class TaskManager(object):
|
||||
"Task not found with uuid of: '%s'" % task)
|
||||
if isinstance(task, Task):
|
||||
try:
|
||||
return settings.TASK_CLASSES[task.task_type](task)
|
||||
return tasks.TASK_CLASSES[task.task_type](task)
|
||||
except KeyError:
|
||||
# TODO(adriant): Maybe we should handle this better
|
||||
# for older deprecated tasks:
|
||||
|
@ -12,15 +12,15 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from adjutant import exceptions
|
||||
from adjutant.tasks.v1.base import BaseTask
|
||||
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, BaseTask):
|
||||
if not issubclass(task_class, base.BaseTask):
|
||||
raise exceptions.InvalidTaskClass(
|
||||
"'%s' is not a built off the BaseTask class."
|
||||
% task_class.__name__
|
||||
@ -30,7 +30,11 @@ def register_task_class(task_class):
|
||||
if task_class.deprecated_task_types:
|
||||
for old_type in task_class.deprecated_task_types:
|
||||
data[old_type] = task_class
|
||||
settings.TASK_CLASSES.update(data)
|
||||
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)
|
||||
|
@ -22,3 +22,18 @@ class CreateProjectAndUser(BaseTask):
|
||||
default_actions = [
|
||||
"NewProjectWithUserAction",
|
||||
]
|
||||
|
||||
email_config = {
|
||||
'initial': {
|
||||
'template': 'create_project_and_user_initial.txt',
|
||||
'subject': 'signup received'
|
||||
},
|
||||
'token': {
|
||||
'template': 'create_project_and_user_token.txt',
|
||||
'subject': 'signup approved'
|
||||
},
|
||||
'completed': {
|
||||
'template': 'create_project_and_user_completed.txt',
|
||||
'subject': 'signup completed'
|
||||
}
|
||||
}
|
||||
|
@ -20,3 +20,12 @@ class UpdateProjectQuotas(BaseTask):
|
||||
default_actions = [
|
||||
"UpdateProjectQuotasAction",
|
||||
]
|
||||
|
||||
email_config = {
|
||||
'initial': None,
|
||||
'token': None,
|
||||
'completed': {
|
||||
'template': 'create_project_and_user_completed.txt',
|
||||
'subject': 'signup completed'
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,18 @@ class InviteUser(BaseTask):
|
||||
"NewUserAction",
|
||||
]
|
||||
|
||||
email_config = {
|
||||
'initial': None,
|
||||
'token': {
|
||||
'template': 'invite_user_to_project_token.txt',
|
||||
'subject': 'invite_user_to_project'
|
||||
},
|
||||
'completed': {
|
||||
'template': 'invite_user_to_project_completed.txt',
|
||||
'subject': 'invite_user_to_project'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ResetUserPassword(BaseTask):
|
||||
task_type = "reset_user_password"
|
||||
@ -31,6 +43,18 @@ class ResetUserPassword(BaseTask):
|
||||
"ResetUserPasswordAction",
|
||||
]
|
||||
|
||||
email_config = {
|
||||
'initial': None,
|
||||
'token': {
|
||||
'template': 'reset_user_password_token.txt',
|
||||
'subject': 'Password Reset for OpenStack'
|
||||
},
|
||||
'completed': {
|
||||
'template': 'reset_user_password_completed.txt',
|
||||
'subject': 'Password Reset for OpenStack'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class EditUserRoles(BaseTask):
|
||||
task_type = "edit_user_roles"
|
||||
@ -39,6 +63,12 @@ class EditUserRoles(BaseTask):
|
||||
"EditUserRolesAction",
|
||||
]
|
||||
|
||||
email_config = {
|
||||
'initial': None,
|
||||
'token': None,
|
||||
'completed': None
|
||||
}
|
||||
|
||||
|
||||
class UpdateUserEmail(BaseTask):
|
||||
task_type = "update_user_email"
|
||||
@ -46,3 +76,26 @@ class UpdateUserEmail(BaseTask):
|
||||
default_actions = [
|
||||
"UpdateUserEmailAction",
|
||||
]
|
||||
additional_actions = [
|
||||
'SendAdditionalEmailAction',
|
||||
]
|
||||
action_config = {
|
||||
'SendAdditionalEmailAction': {
|
||||
'initial': {
|
||||
'subject': 'OpenStack Email Update Requested',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_current_user': True,
|
||||
},
|
||||
},
|
||||
}
|
||||
email_config = {
|
||||
'initial': None,
|
||||
'token': {
|
||||
'subject': 'update_user_email_token',
|
||||
'template': 'update_user_email_token.txt'
|
||||
},
|
||||
'completed': {
|
||||
'subject': 'Email Update Complete',
|
||||
'template': 'update_user_email_completed.txt'
|
||||
}
|
||||
}
|
||||
|
@ -15,17 +15,16 @@
|
||||
from logging import getLogger
|
||||
|
||||
from datetime import timedelta
|
||||
from smtplib import SMTPException
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template import loader
|
||||
from django.utils import timezone
|
||||
|
||||
from adjutant import exceptions
|
||||
from adjutant.api.models import Token
|
||||
from adjutant.api.v1.utils import create_notification
|
||||
from adjutant.notifications.utils import create_notification
|
||||
from adjutant.config import CONF
|
||||
from adjutant import exceptions
|
||||
|
||||
|
||||
LOG = getLogger('adjutant')
|
||||
@ -44,8 +43,10 @@ def handle_task_error(e, task, error_text="while running task"):
|
||||
raise exceptions.TaskActionsFailed(task, internal_message=notes)
|
||||
|
||||
|
||||
def create_token(task):
|
||||
expire = timezone.now() + timedelta(hours=settings.TOKEN_EXPIRE_TIME)
|
||||
def create_token(task, expiry_time=None):
|
||||
if not expiry_time:
|
||||
expiry_time = CONF.workflow.default_token_expiry
|
||||
expire = timezone.now() + timedelta(seconds=expiry_time)
|
||||
|
||||
uuid = uuid4().hex
|
||||
token = Token.objects.create(
|
||||
@ -64,7 +65,7 @@ def send_stage_email(task, email_conf, token=None):
|
||||
text_template = loader.get_template(
|
||||
email_conf['template'],
|
||||
using='include_etc_templates')
|
||||
html_template = email_conf.get('html_template', None)
|
||||
html_template = email_conf['html_template']
|
||||
if html_template:
|
||||
html_template = loader.get_template(
|
||||
html_template,
|
||||
@ -97,15 +98,10 @@ def send_stage_email(task, email_conf, token=None):
|
||||
'actions': actions
|
||||
}
|
||||
if token:
|
||||
if settings.HORIZON_URL:
|
||||
tokenurl = settings.HORIZON_URL
|
||||
if not tokenurl.endswith('/'):
|
||||
tokenurl += '/'
|
||||
tokenurl += 'token/'
|
||||
else:
|
||||
tokenurl = settings.TOKEN_SUBMISSION_URL
|
||||
if not tokenurl.endswith('/'):
|
||||
tokenurl += '/'
|
||||
tokenurl = CONF.workflow.horizon_url
|
||||
if not tokenurl.endswith('/'):
|
||||
tokenurl += '/'
|
||||
tokenurl += 'token/'
|
||||
context.update({
|
||||
'tokenurl': tokenurl,
|
||||
'token': token.token
|
||||
@ -116,7 +112,7 @@ def send_stage_email(task, email_conf, token=None):
|
||||
|
||||
# from_email is the return-path and is distinct from the
|
||||
# message headers
|
||||
from_email = email_conf.get('from')
|
||||
from_email = email_conf['from']
|
||||
if not from_email:
|
||||
from_email = email_conf['reply']
|
||||
elif "%(task_uuid)s" in from_email:
|
||||
@ -145,24 +141,20 @@ def send_stage_email(task, email_conf, token=None):
|
||||
|
||||
email.send(fail_silently=False)
|
||||
|
||||
except SMTPException as e:
|
||||
except Exception as e:
|
||||
notes = {
|
||||
'errors':
|
||||
("Error: '%s' while emailing update for task: %s" %
|
||||
(e, task.uuid))
|
||||
}
|
||||
|
||||
errors_conf = settings.TASK_SETTINGS.get(
|
||||
task.task_type, settings.DEFAULT_TASK_SETTINGS).get(
|
||||
'errors', {}).get("SMTPException", {})
|
||||
notif_conf = task.config.notifications
|
||||
|
||||
if errors_conf:
|
||||
if e.__class__.__name__ in notif_conf.safe_errors:
|
||||
notification = create_notification(
|
||||
task, notes, error=True,
|
||||
engines=errors_conf.get('engines', True))
|
||||
|
||||
if errors_conf.get('notification') == "acknowledge":
|
||||
notification.acknowledged = True
|
||||
notification.save()
|
||||
handlers=False)
|
||||
notification.acknowledged = True
|
||||
notification.save()
|
||||
else:
|
||||
create_notification(task, notes, error=True)
|
||||
|
@ -1,422 +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.
|
||||
|
||||
SECRET_KEY = '+er!4olta#17a=n%uotcazg2ncpl==yjog%1*o-(cr%zys-)!'
|
||||
|
||||
ADDITIONAL_APPS = [
|
||||
'adjutant.api.v1',
|
||||
'adjutant.actions.v1',
|
||||
'adjutant.tasks.v1',
|
||||
]
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': 'db.sqlite3'
|
||||
}
|
||||
}
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'file': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': 'reg_log.log',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'adjutant': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'django': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'keystonemiddleware': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
EMAIL_SETTINGS = {
|
||||
"EMAIL_BACKEND": "django.core.mail.backends.console.EmailBackend"
|
||||
}
|
||||
|
||||
# setting to control if user name and email are allowed
|
||||
# to have different values.
|
||||
USERNAME_IS_EMAIL = True
|
||||
|
||||
# Keystone admin credentials:
|
||||
KEYSTONE = {
|
||||
'username': 'admin',
|
||||
'password': 'openstack',
|
||||
'project_name': 'admin',
|
||||
'auth_url': "http://localhost:5000/v3",
|
||||
}
|
||||
|
||||
HORIZON_URL = 'http://localhost:8080/'
|
||||
|
||||
TOKEN_EXPIRE_TIME = 24
|
||||
|
||||
ACTIVE_DELEGATE_APIS = [
|
||||
'UserRoles',
|
||||
'UserDetail',
|
||||
'UserResetPassword',
|
||||
'UserList',
|
||||
'RoleList',
|
||||
'CreateProjectAndUser',
|
||||
'InviteUser',
|
||||
'ResetPassword',
|
||||
'EditUser',
|
||||
'UpdateEmail',
|
||||
'UpdateProjectQuotas',
|
||||
]
|
||||
|
||||
DEFAULT_TASK_SETTINGS = {
|
||||
'emails': {
|
||||
'token': {
|
||||
'reply': 'no-reply@example.com',
|
||||
'template': 'token.txt',
|
||||
'subject': 'Your Token'
|
||||
},
|
||||
'initial': {
|
||||
'reply': 'no-reply@example.com',
|
||||
'template': 'initial.txt',
|
||||
'subject': 'Initial Confirmation'
|
||||
},
|
||||
'completed': {
|
||||
'reply': 'no-reply@example.com',
|
||||
'template': 'completed.txt',
|
||||
'subject': 'Task completed'
|
||||
}
|
||||
},
|
||||
'notifications': {
|
||||
'EmailNotification': {
|
||||
'standard': {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
'template': 'notification.txt'
|
||||
},
|
||||
'error': {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
'template': 'notification.txt'
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_ACTION_SETTINGS = {
|
||||
'NewProjectAction': {
|
||||
'default_roles': {
|
||||
"project_admin", "project_mod", "_member_", "heat_stack_owner"
|
||||
},
|
||||
},
|
||||
'NewProjectWithUserAction': {
|
||||
'default_roles': {
|
||||
"project_admin", "project_mod", "_member_", "heat_stack_owner"
|
||||
},
|
||||
},
|
||||
'NewUserAction': {
|
||||
'allowed_roles': ['project_mod', 'project_admin', "_member_"]
|
||||
},
|
||||
'NewDefaultNetworkAction': {
|
||||
'RegionOne': {
|
||||
'DNS_NAMESERVERS': ['193.168.1.2', '193.168.1.3'],
|
||||
'SUBNET_CIDR': '192.168.1.0/24',
|
||||
'network_name': 'somenetwork',
|
||||
'public_network': '3cb50f61-5bce-4c03-96e6-8e262e12bb35',
|
||||
'router_name': 'somerouter',
|
||||
'subnet_name': 'somesubnet'
|
||||
},
|
||||
},
|
||||
'NewProjectDefaultNetworkAction': {
|
||||
'RegionOne': {
|
||||
'DNS_NAMESERVERS': ['193.168.1.2', '193.168.1.3'],
|
||||
'SUBNET_CIDR': '192.168.1.0/24',
|
||||
'network_name': 'somenetwork',
|
||||
'public_network': '3cb50f61-5bce-4c03-96e6-8e262e12bb35',
|
||||
'router_name': 'somerouter',
|
||||
'subnet_name': 'somesubnet'
|
||||
},
|
||||
},
|
||||
'SetProjectQuotaAction': {
|
||||
'regions': {
|
||||
'RegionOne': {
|
||||
'quota_size': 'small'
|
||||
},
|
||||
'RegionThree': {
|
||||
'quota_size': 'large_cinder_only'
|
||||
}
|
||||
},
|
||||
},
|
||||
'SendAdditionalEmailAction': {
|
||||
'initial': {
|
||||
'reply': 'no-reply@example.com',
|
||||
'from': 'bounce+%(task_uuid)s@example.com'
|
||||
},
|
||||
'token': {
|
||||
'reply': 'no-reply@example.com',
|
||||
'from': 'bounce+%(task_uuid)s@example.com'
|
||||
},
|
||||
'completed': {
|
||||
'reply': 'no-reply@example.com',
|
||||
'from': 'bounce+%(task_uuid)s@example.com'
|
||||
},
|
||||
},
|
||||
'ResetUserPasswordAction': {
|
||||
'blacklisted_roles': ['admin'],
|
||||
},
|
||||
}
|
||||
|
||||
TASK_SETTINGS = {
|
||||
'invite_user_to_project': {
|
||||
'emails': {
|
||||
'initial': None,
|
||||
'token': {
|
||||
'template': 'invite_user_to_project_token.txt',
|
||||
'subject': 'invite_user_to_project'
|
||||
},
|
||||
'completed': {
|
||||
'template': 'invite_user_to_project_completed.txt',
|
||||
'subject': 'invite_user_to_project'
|
||||
}
|
||||
}
|
||||
},
|
||||
'create_project_and_user': {
|
||||
'emails': {
|
||||
'initial': {
|
||||
'template': 'create_project_and_user_initial.txt',
|
||||
'subject': 'signup received'
|
||||
},
|
||||
'token': {
|
||||
'template': 'create_project_and_user_token.txt',
|
||||
'subject': 'signup approved'
|
||||
},
|
||||
'completed': {
|
||||
'template': 'create_project_and_user_completed.txt',
|
||||
'subject': 'signup completed'
|
||||
}
|
||||
},
|
||||
'additional_actions': [
|
||||
'AddDefaultUsersToProjectAction',
|
||||
'NewProjectDefaultNetworkAction'
|
||||
],
|
||||
'default_region': 'RegionOne',
|
||||
'default_parent_id': None,
|
||||
},
|
||||
'reset_user_password': {
|
||||
'duplicate_policy': 'cancel',
|
||||
'emails': {
|
||||
'initial': None,
|
||||
'token': {
|
||||
'template': 'reset_user_password_token.txt',
|
||||
'subject': 'Password Reset for OpenStack'
|
||||
},
|
||||
'completed': {
|
||||
'template': 'reset_user_password_completed.txt',
|
||||
'subject': 'Password Reset for OpenStack'
|
||||
}
|
||||
}
|
||||
},
|
||||
'update_user_email': {
|
||||
'emails': {
|
||||
'initial': None,
|
||||
'token': {
|
||||
'subject': 'update_user_email_token',
|
||||
'template': 'update_user_email_token.txt'
|
||||
},
|
||||
'completed': {
|
||||
'subject': 'Email Update Complete',
|
||||
'template': 'update_user_email_completed.txt'
|
||||
}
|
||||
},
|
||||
},
|
||||
'edit_user_roles': {
|
||||
'role_blacklist': ['admin']
|
||||
},
|
||||
'update_quota': {
|
||||
'duplicate_policy': 'cancel',
|
||||
'days_between_autoapprove': 30,
|
||||
},
|
||||
}
|
||||
|
||||
ROLES_MAPPING = {
|
||||
'admin': [
|
||||
'project_admin', 'project_mod', '_member_', 'heat_stack_owner'
|
||||
],
|
||||
'project_admin': [
|
||||
'project_mod', '_member_', 'heat_stack_owner', 'project_admin',
|
||||
],
|
||||
'project_mod': [
|
||||
'_member_', 'heat_stack_owner', 'project_mod',
|
||||
],
|
||||
}
|
||||
|
||||
PROJECT_QUOTA_SIZES = {
|
||||
'small': {
|
||||
'nova': {
|
||||
'instances': 10,
|
||||
'cores': 20,
|
||||
'ram': 65536,
|
||||
'floating_ips': 10,
|
||||
'fixed_ips': 0,
|
||||
'metadata_items': 128,
|
||||
'injected_files': 5,
|
||||
'injected_file_content_bytes': 10240,
|
||||
'key_pairs': 50,
|
||||
'security_groups': 20,
|
||||
'security_group_rules': 100,
|
||||
},
|
||||
'cinder': {
|
||||
'gigabytes': 5000,
|
||||
'snapshots': 50,
|
||||
'volumes': 20,
|
||||
},
|
||||
'neutron': {
|
||||
'floatingip': 10,
|
||||
'network': 3,
|
||||
'port': 50,
|
||||
'router': 3,
|
||||
'security_group': 20,
|
||||
'security_group_rule': 100,
|
||||
'subnet': 3,
|
||||
},
|
||||
"octavia": {
|
||||
'health_monitor': 5,
|
||||
"listener": 1,
|
||||
"load_balancer": 1,
|
||||
"member": 2,
|
||||
"pool": 1,
|
||||
},
|
||||
},
|
||||
"medium": {
|
||||
"cinder": {
|
||||
"gigabytes": 10000,
|
||||
"volumes": 100,
|
||||
"snapshots": 300
|
||||
},
|
||||
"nova": {
|
||||
"metadata_items": 128,
|
||||
"injected_file_content_bytes": 10240,
|
||||
"ram": 327680,
|
||||
"floating_ips": 25,
|
||||
"key_pairs": 50,
|
||||
"instances": 50,
|
||||
"security_group_rules": 400,
|
||||
"injected_files": 5,
|
||||
"cores": 100,
|
||||
"fixed_ips": 0,
|
||||
"security_groups": 50
|
||||
},
|
||||
"neutron": {
|
||||
"security_group_rule": 400,
|
||||
"subnet": 5,
|
||||
"network": 5,
|
||||
"floatingip": 25,
|
||||
"security_group": 50,
|
||||
"router": 5,
|
||||
"port": 250
|
||||
},
|
||||
"octavia": {
|
||||
'health_monitor': 50,
|
||||
"listener": 5,
|
||||
"load_balancer": 5,
|
||||
"member": 5,
|
||||
"pool": 5,
|
||||
},
|
||||
},
|
||||
"large": {
|
||||
"cinder": {
|
||||
"gigabytes": 50000,
|
||||
"volumes": 200,
|
||||
"snapshots": 600
|
||||
},
|
||||
"nova": {
|
||||
"metadata_items": 128,
|
||||
"injected_file_content_bytes": 10240,
|
||||
"ram": 655360,
|
||||
"floating_ips": 50,
|
||||
"key_pairs": 50,
|
||||
"instances": 100,
|
||||
"security_group_rules": 800,
|
||||
"injected_files": 5,
|
||||
"cores": 200,
|
||||
"fixed_ips": 0,
|
||||
"security_groups": 100
|
||||
},
|
||||
"neutron": {
|
||||
"security_group_rule": 800,
|
||||
"subnet": 10,
|
||||
"network": 10,
|
||||
"floatingip": 50,
|
||||
"security_group": 100,
|
||||
"router": 10,
|
||||
"port": 500
|
||||
},
|
||||
"octavia": {
|
||||
'health_monitor': 100,
|
||||
"listener": 10,
|
||||
"load_balancer": 10,
|
||||
"member": 10,
|
||||
"pool": 10,
|
||||
},
|
||||
},
|
||||
"large_cinder_only": {
|
||||
"cinder": {
|
||||
"gigabytes": 50001,
|
||||
"volumes": 200,
|
||||
"snapshots": 600
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
QUOTA_SIZES_ASC = ['small', 'medium', 'large']
|
||||
|
||||
QUOTA_SERVICES = {'*': ['cinder', 'neutron', 'nova']}
|
||||
|
||||
SHOW_ACTION_ENDPOINTS = True
|
||||
|
||||
TOKEN_CACHE_TIME = 60
|
||||
|
||||
conf_dict = {
|
||||
"DEBUG": True,
|
||||
"SECRET_KEY": SECRET_KEY,
|
||||
"ADDITIONAL_APPS": ADDITIONAL_APPS,
|
||||
"DATABASES": DATABASES,
|
||||
"LOGGING": LOGGING,
|
||||
"EMAIL_SETTINGS": EMAIL_SETTINGS,
|
||||
"USERNAME_IS_EMAIL": USERNAME_IS_EMAIL,
|
||||
"KEYSTONE": KEYSTONE,
|
||||
"ACTIVE_DELEGATE_APIS": ACTIVE_DELEGATE_APIS,
|
||||
"DEFAULT_TASK_SETTINGS": DEFAULT_TASK_SETTINGS,
|
||||
"TASK_SETTINGS": TASK_SETTINGS,
|
||||
"DEFAULT_ACTION_SETTINGS": DEFAULT_ACTION_SETTINGS,
|
||||
"HORIZON_URL": HORIZON_URL,
|
||||
"TOKEN_EXPIRE_TIME": TOKEN_EXPIRE_TIME,
|
||||
"ROLES_MAPPING": ROLES_MAPPING,
|
||||
"PROJECT_QUOTA_SIZES": PROJECT_QUOTA_SIZES,
|
||||
"SHOW_ACTION_ENDPOINTS": SHOW_ACTION_ENDPOINTS,
|
||||
"QUOTA_SIZES_ASC": QUOTA_SIZES_ASC,
|
||||
"TOKEN_CACHE_TIME": TOKEN_CACHE_TIME,
|
||||
"QUOTA_SERVICES": QUOTA_SERVICES,
|
||||
}
|
@ -1,48 +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 copy import deepcopy
|
||||
|
||||
|
||||
def dict_merge(a, b):
|
||||
"""
|
||||
Recursively merges two dicts.
|
||||
If both a and b have a key who's value is a dict then dict_merge is called
|
||||
on both values and the result stored in the returned dictionary.
|
||||
B is the override.
|
||||
"""
|
||||
if not isinstance(b, dict):
|
||||
return b
|
||||
result = deepcopy(a)
|
||||
for k, v in b.items():
|
||||
if k in result and isinstance(result[k], dict):
|
||||
result[k] = dict_merge(result[k], v)
|
||||
else:
|
||||
result[k] = deepcopy(v)
|
||||
return result
|
||||
|
||||
|
||||
def setup_task_settings(task_defaults, action_defaults, task_settings):
|
||||
"""
|
||||
Cascading merge of the default settings, and the
|
||||
settings for each task_type.
|
||||
"""
|
||||
new_task_settings = {}
|
||||
for task, settings in task_settings.items():
|
||||
task_setting = deepcopy(task_defaults)
|
||||
task_setting['action_settings'] = deepcopy(action_defaults)
|
||||
new_task_settings[task] = dict_merge(task_setting, settings)
|
||||
|
||||
return new_task_settings
|
@ -22,10 +22,13 @@ https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
from django.conf import settings
|
||||
|
||||
from keystonemiddleware.auth_token import AuthProtocol
|
||||
|
||||
from adjutant.config import CONF
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "adjutant.settings")
|
||||
|
||||
|
||||
@ -35,14 +38,14 @@ application = get_wsgi_application()
|
||||
# the Keystone Auth Middleware.
|
||||
conf = {
|
||||
"auth_plugin": "password",
|
||||
'username': settings.KEYSTONE['username'],
|
||||
'password': settings.KEYSTONE['password'],
|
||||
'project_name': settings.KEYSTONE['project_name'],
|
||||
"project_domain_id": settings.KEYSTONE.get('domain_id', "default"),
|
||||
"user_domain_id": settings.KEYSTONE.get('domain_id', "default"),
|
||||
"auth_url": settings.KEYSTONE['auth_url'],
|
||||
'username': CONF.identity.auth.username,
|
||||
'password': CONF.identity.auth.password,
|
||||
'project_name': CONF.identity.auth.project_name,
|
||||
"project_domain_id": CONF.identity.auth.project_domain_id,
|
||||
"user_domain_id": CONF.identity.auth.user_domain_id,
|
||||
"auth_url": CONF.identity.auth.auth_url,
|
||||
'delay_auth_decision': True,
|
||||
'include_service_catalog': False,
|
||||
'token_cache_time': settings.TOKEN_CACHE_TIME,
|
||||
'token_cache_time': CONF.identity.token_cache_time,
|
||||
}
|
||||
application = AuthProtocol(application, conf)
|
||||
|
@ -34,7 +34,7 @@ Response Example
|
||||
"name": "demo",
|
||||
"roles": [
|
||||
"project_admin",
|
||||
"__member__"
|
||||
"_member_"
|
||||
],
|
||||
"status": "Active"
|
||||
}
|
||||
@ -64,7 +64,7 @@ Request Example
|
||||
|
||||
curl -H "X-Auth-Token: $NOS_TOKEN" http://0.0.0.0:5050/v1/openstack/users \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"roles": ["_member_"], "email": "new@example.com"}'
|
||||
-d '{"roles": ["member"], "email": "new@example.com"}'
|
||||
|
||||
Response Example
|
||||
-----------------
|
||||
@ -216,7 +216,7 @@ Response Example
|
||||
"links": {
|
||||
"self": "http://identity/v3/roles/9fe2ff9ee4384b1894a90878d3e92bab"
|
||||
},
|
||||
"name": "_member_"
|
||||
"name": "member"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
414
conf/conf.yaml
414
conf/conf.yaml
@ -1,414 +0,0 @@
|
||||
# General settings
|
||||
SECRET_KEY: '+er!!4olta#17a=n%uotcazg2ncpl==yjog%1*o-(cr%zys-)!'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG: True
|
||||
ALLOWED_HOSTS:
|
||||
- "*"
|
||||
|
||||
ADDITIONAL_APPS:
|
||||
- adjutant.api.v1
|
||||
- adjutant.tasks.v1
|
||||
- adjutant.actions.v1
|
||||
|
||||
DATABASES:
|
||||
default:
|
||||
ENGINE: django.db.backends.sqlite3
|
||||
NAME: db.sqlite3
|
||||
|
||||
LOGGING:
|
||||
version: 1
|
||||
disable_existing_loggers: False
|
||||
handlers:
|
||||
file:
|
||||
level: INFO
|
||||
class: logging.FileHandler
|
||||
filename: reg_log.log
|
||||
loggers:
|
||||
adjutant:
|
||||
handlers:
|
||||
- file
|
||||
level: INFO
|
||||
propagate: False
|
||||
django:
|
||||
handlers:
|
||||
- file
|
||||
level: INFO
|
||||
propagate: False
|
||||
keystonemiddleware:
|
||||
handlers:
|
||||
- file
|
||||
level: INFO
|
||||
propagate: False
|
||||
|
||||
EMAIL_SETTINGS:
|
||||
EMAIL_BACKEND: django.core.mail.backends.console.EmailBackend
|
||||
|
||||
# setting to control if user name and email are allowed
|
||||
# to have different values.
|
||||
USERNAME_IS_EMAIL: True
|
||||
|
||||
# Keystone config
|
||||
KEYSTONE:
|
||||
username: admin
|
||||
password: openstack
|
||||
project_name: admin
|
||||
# MUST BE V3 API:
|
||||
auth_url: http://localhost/identity/v3
|
||||
domain_id: default
|
||||
can_edit_users: True
|
||||
|
||||
HORIZON_URL: http://localhost:8080/
|
||||
|
||||
# time for the token to expire in hours
|
||||
TOKEN_EXPIRE_TIME: 24
|
||||
|
||||
ACTIVE_DELEGATE_APIS:
|
||||
- UserRoles
|
||||
- UserDetail
|
||||
- UserResetPassword
|
||||
- UserList
|
||||
- RoleList
|
||||
- SignUp
|
||||
- UserUpdateEmail
|
||||
- UpdateProjectQuotas
|
||||
|
||||
DEFAULT_TASK_SETTINGS:
|
||||
emails:
|
||||
initial:
|
||||
subject: Initial Confirmation
|
||||
reply: no-reply@example.com
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
template: initial.txt
|
||||
# html_template: initial.txt
|
||||
# If the related actions 'can' send a token,
|
||||
# this field should here.
|
||||
token:
|
||||
subject: Your Token
|
||||
reply: no-reply@example.com
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
template: token.txt
|
||||
# html_template: token.txt
|
||||
completed:
|
||||
subject: Task completed
|
||||
reply: no-reply@example.com
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
template: completed.txt
|
||||
# html_template: completed.txt
|
||||
notifications:
|
||||
EmailNotification:
|
||||
standard:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
template: notification.txt
|
||||
# html_template: completed.txt
|
||||
error:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
template: notification.txt
|
||||
# html_template: completed.txt
|
||||
|
||||
# Default Action settings:
|
||||
# These can be overridden at a per task level below in the
|
||||
# task settings so that multiple tasks can use the same actions
|
||||
# slightly differently.
|
||||
#
|
||||
# TASK_SETTINGS:
|
||||
# <task_type>:
|
||||
# <othersettings> ....
|
||||
# ....
|
||||
# action_settings:
|
||||
# <action_class_name>:
|
||||
# <action_settings_overrides> ....
|
||||
DEFAULT_ACTION_SETTINGS:
|
||||
NewProjectAction:
|
||||
default_roles:
|
||||
- project_admin
|
||||
- project_mod
|
||||
- heat_stack_owner
|
||||
- _member_
|
||||
NewProjectWithUserAction:
|
||||
default_roles:
|
||||
- project_admin
|
||||
- project_mod
|
||||
- heat_stack_owner
|
||||
- _member_
|
||||
NewUserAction:
|
||||
allowed_roles:
|
||||
- project_admin
|
||||
- project_mod
|
||||
- heat_stack_owner
|
||||
- _member_
|
||||
ResetUserPasswordAction:
|
||||
blacklisted_roles:
|
||||
- admin
|
||||
NewDefaultNetworkAction:
|
||||
RegionOne:
|
||||
network_name: default_network
|
||||
subnet_name: default_subnet
|
||||
router_name: default_router
|
||||
public_network: 3cb50d61-5bce-4c03-96e6-8e262e12bb35
|
||||
DNS_NAMESERVERS:
|
||||
- 193.168.1.2
|
||||
- 193.168.1.3
|
||||
SUBNET_CIDR: 192.168.1.0/24
|
||||
NewProjectDefaultNetworkAction:
|
||||
RegionOne:
|
||||
network_name: default_network
|
||||
subnet_name: default_subnet
|
||||
router_name: default_router
|
||||
public_network: 3cb50d61-5bce-4c03-96e6-8e262e12bb35
|
||||
DNS_NAMESERVERS:
|
||||
- 193.168.1.2
|
||||
- 193.168.1.3
|
||||
SUBNET_CIDR: 192.168.1.0/24
|
||||
AddDefaultUsersToProjectAction:
|
||||
default_users:
|
||||
- admin
|
||||
default_roles:
|
||||
- admin
|
||||
SetProjectQuotaAction:
|
||||
regions:
|
||||
RegionOne:
|
||||
quota_size: small
|
||||
UpdateProjectQuotasAction:
|
||||
days_between_autoapprove: 30
|
||||
SendAdditionalEmailAction:
|
||||
initial:
|
||||
email_current_user: False
|
||||
reply: no-reply@example.com
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
subject: "Openstack Email Notification"
|
||||
template: null
|
||||
token:
|
||||
email_current_user: False
|
||||
reply: no-reply@example.com
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
subject: "Openstack Email Notification"
|
||||
template: null
|
||||
completed:
|
||||
email_current_user: False
|
||||
reply: no-reply@example.com
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
subject: "Openstack Email Notification"
|
||||
template: null
|
||||
# A null template will cause the email not to send
|
||||
# Also emails to the given roles on the project
|
||||
# email_roles:
|
||||
# - project_admin
|
||||
# Or sends to an email set in the task cache
|
||||
# email_task_cache: True
|
||||
# Or sends to an arbitrary admin email
|
||||
# email_additional_addresses:
|
||||
# - admin@example.org
|
||||
|
||||
# These are cascading overrides for the default settings:
|
||||
TASK_SETTINGS:
|
||||
create_project_and_user:
|
||||
# Additional actions for task
|
||||
# These will run after the default actions, in the given order.
|
||||
additional_actions:
|
||||
- NewProjectDefaultNetworkAction
|
||||
- SetProjectQuotaAction
|
||||
emails:
|
||||
initial:
|
||||
subject: Your OpenStack signup has been received
|
||||
template: create_project_and_user_initial.txt
|
||||
token:
|
||||
subject: Your OpenStack signup has been approved
|
||||
template: create_project_and_user_token.txt
|
||||
completed:
|
||||
subject: Your OpenStack signup has been completed
|
||||
template: create_project_and_user_completed.txt
|
||||
notifications:
|
||||
EmailNotification:
|
||||
standard:
|
||||
emails:
|
||||
- signups@example.com
|
||||
error:
|
||||
emails:
|
||||
- signups@example.com
|
||||
default_region: RegionOne
|
||||
# If 'None' (null in yaml) will default to domain as parent.
|
||||
# If domain isn't set explicity will service user domain (see KEYSTONE).
|
||||
default_parent_id: null
|
||||
invite_user_to_project:
|
||||
duplicate_policy: cancel
|
||||
emails:
|
||||
# To not send this email set the value to null
|
||||
initial: null
|
||||
token:
|
||||
subject: Invitation to an OpenStack project
|
||||
template: invite_user_to_project_token.txt
|
||||
completed:
|
||||
subject: Invitation Completed
|
||||
template: invite_user_to_project_completed.txt
|
||||
errors:
|
||||
SMTPException:
|
||||
notification: acknowledge
|
||||
engines: False
|
||||
reset_user_password:
|
||||
duplicate_policy: cancel
|
||||
emails:
|
||||
initial: null
|
||||
token:
|
||||
subject: Password Reset for OpenStack
|
||||
template: reset_user_password_token.txt
|
||||
completed:
|
||||
subject: Password Reset Completed
|
||||
template: reset_user_password_completed.txt
|
||||
edit_user_roles:
|
||||
duplicate_policy: cancel
|
||||
emails:
|
||||
initial: null
|
||||
token: null
|
||||
role_blacklist:
|
||||
- admin
|
||||
edit_roles:
|
||||
duplicate_policy: cancel
|
||||
emails:
|
||||
initial: null
|
||||
token: null
|
||||
update_user_email:
|
||||
duplicate_policy: cancel
|
||||
additional_actions:
|
||||
- SendAdditionalEmailAction
|
||||
emails:
|
||||
initial: null
|
||||
token:
|
||||
subject: Confirm OpenStack Email Update
|
||||
template: update_user_email_token.txt
|
||||
completed:
|
||||
subject: OpenStack Email Updated
|
||||
template: update_user_email_completed.txt
|
||||
action_settings:
|
||||
SendAdditionalEmailAction:
|
||||
initial:
|
||||
subject: OpenStack Email Update Requested
|
||||
template: update_user_email_started.txt
|
||||
email_current_user: True
|
||||
update_quota:
|
||||
duplicate_policy: cancel
|
||||
size_difference_threshold: 0.1
|
||||
emails:
|
||||
initial: null
|
||||
token: null
|
||||
completed:
|
||||
subject: Openstack Quota updated
|
||||
template: update_quota_completed.txt
|
||||
|
||||
# mapping between roles and managable roles
|
||||
ROLES_MAPPING:
|
||||
admin:
|
||||
- project_admin
|
||||
- project_mod
|
||||
- heat_stack_owner
|
||||
- _member_
|
||||
project_admin:
|
||||
- project_admin
|
||||
- project_mod
|
||||
- heat_stack_owner
|
||||
- _member_
|
||||
project_mod:
|
||||
- project_mod
|
||||
- heat_stack_owner
|
||||
- _member_
|
||||
|
||||
PROJECT_QUOTA_SIZES:
|
||||
small:
|
||||
nova:
|
||||
instances: 10
|
||||
cores: 20
|
||||
ram: 65536
|
||||
floating_ips: 10
|
||||
fixed_ips: 0
|
||||
metadata_items: 128
|
||||
injected_files: 5
|
||||
injected_file_content_bytes: 10240
|
||||
key_pairs: 50
|
||||
security_groups: 20
|
||||
security_group_rules: 100
|
||||
cinder:
|
||||
gigabytes: 5000
|
||||
snapshots: 50
|
||||
volumes: 20
|
||||
neutron:
|
||||
floatingip: 10
|
||||
network: 3
|
||||
port: 50
|
||||
router: 3
|
||||
security_group: 20
|
||||
security_group_rule: 100
|
||||
subnet: 3
|
||||
medium:
|
||||
cinder:
|
||||
gigabytes: 10000
|
||||
volumes: 100
|
||||
snapshots: 300
|
||||
nova:
|
||||
metadata_items: 128
|
||||
injected_file_content_bytes: 10240
|
||||
ram: 327680
|
||||
floating_ips: 25
|
||||
key_pairs: 50
|
||||
instances: 50
|
||||
security_group_rules: 400
|
||||
injected_files: 5
|
||||
cores: 100
|
||||
fixed_ips: 0
|
||||
security_groups: 50
|
||||
neutron:
|
||||
security_group_rule: 400
|
||||
subnet: 5
|
||||
network: 5
|
||||
floatingip: 25
|
||||
security_group: 50
|
||||
router: 5
|
||||
port: 250
|
||||
large:
|
||||
cinder:
|
||||
gigabytes: 50000
|
||||
volumes: 200
|
||||
snapshots: 600
|
||||
nova:
|
||||
metadata_items: 128
|
||||
injected_file_content_bytes: 10240
|
||||
ram: 655360
|
||||
floating_ips: 50
|
||||
key_pairs: 50
|
||||
instances: 100
|
||||
security_group_rules: 800
|
||||
injected_files: 5
|
||||
cores: 200
|
||||
fixed_ips: 0
|
||||
security_groups: 100
|
||||
neutron:
|
||||
security_group_rule: 800
|
||||
subnet: 10
|
||||
network: 10
|
||||
floatingip: 50
|
||||
security_group: 100
|
||||
router: 10
|
||||
port: 500
|
||||
|
||||
# Time in seconds to cache token from Keystone
|
||||
TOKEN_CACHE_TIME: 600
|
||||
|
||||
# Ordered list of quota sizes from smallest to biggest
|
||||
QUOTA_SIZES_ASC:
|
||||
- small
|
||||
- medium
|
||||
- large
|
||||
|
||||
# Services to check through the quotas for
|
||||
QUOTA_SERVICES:
|
||||
"*":
|
||||
- nova
|
||||
- neutron
|
||||
- cinder
|
||||
# Additonal Quota Service
|
||||
# - octavia
|
@ -1,264 +1,84 @@
|
||||
Configuring Adjutant
|
||||
====================
|
||||
|
||||
.. highlight:: yaml
|
||||
|
||||
Adjutant is designed to be highly configurable for various needs. The goal
|
||||
of Adjutant is to provide a variety of common tasks and actions that can
|
||||
be easily extended or changed based upon the needs of your OpenStack.
|
||||
be easily extended or changed based upon the needs of your OpenStack cluster.
|
||||
|
||||
The default Adjutant configuration is found in conf/conf.yaml, and but will
|
||||
be overridden if a file is placed at ``/etc/adjutant/conf.yaml``.
|
||||
For configuration Adjutant uses a library called CONFspirator to define and
|
||||
register our config values. This makes the app better at processing defaults
|
||||
and checking the validity of the config.
|
||||
|
||||
The first part of the configuration file contains standard Django settings.
|
||||
An example Adjutant config file is found in conf/adjutant.yaml, and a new one
|
||||
can be generated by running::
|
||||
|
||||
.. code-block:: yaml
|
||||
tox -e venv -- adjutant-api exampleconfig --output-file /etc/adjutant/adjutant.yaml
|
||||
|
||||
SECRET_KEY:
|
||||
With ``--output-file`` controlling where the file goes.
|
||||
|
||||
ALLOWED_HOSTS:
|
||||
- "*"
|
||||
This example file should be your starting point for configuring the service,
|
||||
and your core source of documentation for what each config does.
|
||||
|
||||
ADDITIONAL_APPS:
|
||||
- adjutant.api.v1
|
||||
- adjutant.tasks.v1
|
||||
- adjutant.actions.v1
|
||||
Adjutant will read the file from ``/etc/adjutant/adjutant.yaml`` or if the
|
||||
environment variable ``ADJUTANT_CONFIG_FILE`` is set, will look for the file
|
||||
in the specified location.
|
||||
|
||||
DATABASES:
|
||||
default:
|
||||
ENGINE: django.db.backends.sqlite3
|
||||
NAME: db.sqlite3
|
||||
Configuration options
|
||||
+++++++++++++++++++++
|
||||
|
||||
LOGGING:
|
||||
...
|
||||
|
||||
EMAIL_SETTINGS:
|
||||
EMAIL_BACKEND: django.core.mail.backends.console.EmailBackend
|
||||
|
||||
|
||||
If you have any plugins, ensure that they are also added to
|
||||
**ADDITIONAL_APPS**.
|
||||
|
||||
API Settings
|
||||
Django group
|
||||
------------
|
||||
|
||||
The next part of the confirguration file contains a number of settings
|
||||
for all APIs.
|
||||
The first part of the configuration file contains standard Django settings,
|
||||
and for the most part the generated example config will explain all the
|
||||
options.
|
||||
|
||||
.. code-block:: yaml
|
||||
Identity group
|
||||
--------------
|
||||
|
||||
USERNAME_IS_EMAIL: True
|
||||
Are the configs for how Adjutant interacts with Keystone, with the important
|
||||
ones being as follows:
|
||||
|
||||
KEYSTONE:
|
||||
username:
|
||||
password:
|
||||
project_name:
|
||||
auth_url: http://localhost:5000/v3
|
||||
domain_id: default
|
||||
**adjutant.identity.username_is_email** impacts account creation, and email
|
||||
modification actions. In the case that it is true, any task passing a username
|
||||
and email pair, the username will be ignored. This also impacts where emails
|
||||
are sent to.
|
||||
|
||||
HORIZON_URL: http://192.168.122.160:8080/token/
|
||||
**adjutant.identity.auth** Are the credentials that Adjutant uses to talk to
|
||||
Keystone, and the various other OpenStack services.
|
||||
|
||||
# default time for the token to expire in hours
|
||||
TOKEN_EXPIRE_TIME: 24
|
||||
**adjutant.identity.role_mapping** defines which roles can modify other roles.
|
||||
In the default configuration a user who has the role project_mod will not be
|
||||
able to modify any of the roles for a user with the project_admin role.
|
||||
|
||||
ROLES_MAPPING:
|
||||
admin:
|
||||
- project_admin
|
||||
- project_mod
|
||||
- _member_
|
||||
project_admin:
|
||||
- project_admin
|
||||
- project_mod
|
||||
- _member_
|
||||
project_mod:
|
||||
- project_mod
|
||||
- heat_stack_owner
|
||||
- _member_
|
||||
API group
|
||||
---------
|
||||
|
||||
ACTIVE_DELEGATE_APIS:
|
||||
- UserRoles
|
||||
- UserDetail
|
||||
- UserResetPassword
|
||||
- UserList
|
||||
- RoleList
|
||||
- SignUp
|
||||
- UserUpdateEmail
|
||||
Controls which DelegateAPIs are enabled, and what some of their configuration
|
||||
may be.
|
||||
|
||||
**USERNAME_IS_EMAIL** impacts account creation, and email modification actions.
|
||||
In the case that it is true, any task passing a username and email pair, the
|
||||
username will be ignored. This also impacts where emails are sent to.
|
||||
Notifications group
|
||||
-------------------
|
||||
|
||||
The keystone settings must be for a user with administrative privileges,
|
||||
and must use the Keystone V3 API endpoint.
|
||||
Default settings around what notifications should do during the task workflows.
|
||||
|
||||
If you have Horizon configured with adjutant-api **TOKEN_SUBMISSION_URL**
|
||||
should point to that.
|
||||
Workflow group
|
||||
--------------
|
||||
|
||||
**ROLES_MAPPING** defines which roles can modify other roles. In the default
|
||||
configuration a user who has the role project_mod will not be able to
|
||||
modify any of the roles for a user with the project_admin role.
|
||||
|
||||
**ACTIVE_DELEGATE_APIS** defines all in use DelegateAPIs, including those that
|
||||
are from plugins must be included in this list. If a task is removed from this
|
||||
list its endpoint will not be accessable however users who have started tasks
|
||||
will still be able submit them.
|
||||
|
||||
Standard Task Settings
|
||||
----------------------
|
||||
|
||||
The DelegateAPIs are built around the task layer, and the tasks themselves
|
||||
have their own configuration.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
DEFAULT_TASK_SETTINGS:
|
||||
duplicate_policy: null
|
||||
emails:
|
||||
initial:
|
||||
subject: Initial Confirmation
|
||||
reply: no-reply@example.com
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
template: initial.txt
|
||||
# html_template: initial.txt
|
||||
token:
|
||||
|
||||
completed:
|
||||
notifications:
|
||||
EmailNotification:
|
||||
standard:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
template: notification.txt
|
||||
# html_template: completed.txt
|
||||
error:
|
||||
|
||||
**DEFAULT_TASK_SETTINGS** Represents the default settings for all task
|
||||
unless otherwise overridden for individual tasks in the TASK_SETTINGS
|
||||
configuration, these are cascading overrides. Two additional options
|
||||
are available, overriding the default actions or adding in additional
|
||||
actions. These will run in the order specified.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
TASK_SETTINGS:
|
||||
create_project_and_user:
|
||||
default_actions:
|
||||
- NewProjectAction
|
||||
invite_user_to_project:
|
||||
additional_actions:
|
||||
- SendAdditionalEmailAction
|
||||
|
||||
|
||||
By default duplicate tasks will be marked as invalid, however the duplicate
|
||||
policy can be set to 'cancel' to cancel duplicates and start a new class.
|
||||
|
||||
You can also here at the task settings layer ensure that the task is never auto
|
||||
approved by it's underlying actions.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
TASK_SETTINGS:
|
||||
update_quota:
|
||||
allow_auto_approve: False
|
||||
|
||||
|
||||
Email Settings
|
||||
~~~~~~~~~~~~~~
|
||||
The ``initial`` email will be sent after the user makes the request, the
|
||||
``token`` email will be sent after approval steps are run, and the
|
||||
``completed`` email will be sent after the token is submitted.
|
||||
|
||||
The emails will be sent to the current user, however this can be changed at
|
||||
the action level with the ``get_email()`` function.
|
||||
|
||||
Notification Settings
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
The type of notifications can be defined here for both standard notifications
|
||||
and error notifications::
|
||||
|
||||
notifications:
|
||||
EmailNotification:
|
||||
standard:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
template: notification.txt
|
||||
error:
|
||||
emails:
|
||||
- errors@example.com
|
||||
reply: no-reply@example.com
|
||||
template: notification.txt
|
||||
<other notification engine>:
|
||||
|
||||
Currently EmailNotification is the only available notification engine however
|
||||
new engines can be added through plugins and may have different settings.
|
||||
|
||||
|
||||
Action Settings
|
||||
---------------
|
||||
**adjutant.workflow.task_defaults** Represents the default settings for all
|
||||
tasks unless otherwise overridden for individual tasks in
|
||||
``adjutant.workflow.tasks``.
|
||||
|
||||
Default action settings.
|
||||
Actions will each have their own specific settings, dependent on what they
|
||||
are for. The standard settings for a number of default actions are below:
|
||||
**adjutant.workflow.action_defaults** Are the default settings for each action
|
||||
and can be overriden on a per task basis via
|
||||
``adjutant.workflow.tasks.<my_task>.actions``.
|
||||
|
||||
An action can have it's settings overridden in the settings for it's task.
|
||||
This will only effect when the action is called through that specific task
|
||||
Overriding action settings for a specific task.
|
||||
|
||||
Email Templates
|
||||
---------------
|
||||
Email and notification templates
|
||||
++++++++++++++++++++++++++++++++
|
||||
|
||||
Additional templates can be placed in ``/etc/adjutant/templates/`` and will be
|
||||
loaded in automatically. A plain text template and an HTML template can be
|
||||
specified separately. The context for this will include the task object and
|
||||
a dictionary containing the action objects.
|
||||
|
||||
Additional Emails
|
||||
------------------
|
||||
|
||||
The SendAdditionalEmailAction is designed to be added in at configuration
|
||||
for relevant tasks. It's templates are also passed a context dictionary with
|
||||
the task and actions available. By default the template is null and the email
|
||||
will not send.
|
||||
|
||||
The settings for this action should be defined within the action_settings
|
||||
for its related task.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
additional_actions:
|
||||
- SendAdditionalEmailAction
|
||||
action_settings:
|
||||
SendAdditionalEmailAction:
|
||||
initial:
|
||||
subject: OpenStack Email Update Requested
|
||||
template: update_user_email_started.txt
|
||||
email_current_user: True
|
||||
|
||||
The additional email action can also send to a subset of people.
|
||||
|
||||
The user who made the request can be emailed with::
|
||||
|
||||
email_current_user: True
|
||||
|
||||
Or the email can be sent to everyone who has a certain role on the project.
|
||||
(Multiple roles can also be specified)
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
email_roles:
|
||||
- project_admin
|
||||
|
||||
Or an email can be sent to a specified address in the task cache
|
||||
(key: ``additional_emails``) ::
|
||||
|
||||
email_in_task_cache: True
|
||||
|
||||
Or sent to an arbitrary administrative email address(es)::
|
||||
|
||||
email_additional_addresses:
|
||||
- admin@example.org
|
||||
|
||||
This can be useful in the case of large project affecting actions.
|
||||
|
@ -17,11 +17,11 @@ Building DelegateAPIs
|
||||
New DelegateAPIs should inherit from adjutant.api.v1.base.BaseDelegateAPI
|
||||
can be registered as such::
|
||||
|
||||
from adjutant.api.v1.models import register_delegate_api_class,
|
||||
from adjutant.plugins import register_plugin_delegate_api,
|
||||
|
||||
from myplugin import apis
|
||||
|
||||
register_delegate_api_class(r'^my-plugin/some-action/?$', apis.MyAPIView)
|
||||
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.
|
||||
@ -55,9 +55,9 @@ Building Tasks
|
||||
Tasks must be derived from adjutant.tasks.v1.base.BaseTask and can be
|
||||
registered as such::
|
||||
|
||||
from adjutant.tasks.v1.models import register_task_class
|
||||
from adjutant.plugins import register_plugin_task
|
||||
|
||||
register_task_class(MyPluginTask)
|
||||
register_plugin_task(MyPluginTask)
|
||||
|
||||
Examples of tasks can be found in `adjutant.tasks.v1`
|
||||
|
||||
@ -77,9 +77,9 @@ Building Actions
|
||||
Actions must be derived from adjutant.actions.v1.base.BaseAction and are
|
||||
registered alongside their serializer::
|
||||
|
||||
from adjutant.actions.v1.models import register_action_class
|
||||
from adjutant.plugins import register_plugin_action
|
||||
|
||||
register_action_class(MyCustomAction, MyCustomActionSerializer)
|
||||
register_action_class(MyCustomAction, MyCustomActionSerializer)
|
||||
|
||||
Serializers can inherit from either rest_framework.serializers.Serializer, or
|
||||
the current serializers in adjutant.actions.v1.serializers.
|
||||
@ -144,34 +144,54 @@ Example::
|
||||
value_1 = serializers.CharField()
|
||||
|
||||
******************************
|
||||
Building Notification Engines
|
||||
Building Notification Handlers
|
||||
******************************
|
||||
|
||||
Notification Engines can also be added through a plugin::
|
||||
Notification Handlers can also be added through a plugin::
|
||||
|
||||
from adjutant.notifcations.models import NotificationEngine
|
||||
from django.conf import settings
|
||||
from adjutant.notifications.models import BaseNotificationHandler
|
||||
from adjutant.plugins import register_notification_handler
|
||||
|
||||
class NewNotificationEngine(NotificationEngine):
|
||||
class NewNotificationHandler(BaseNotificationHandler):
|
||||
|
||||
settings_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.BoolConfig(
|
||||
"do_this_thing",
|
||||
help_text="Should we do the thing?",
|
||||
default=False,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def _notify(self, task, notification):
|
||||
if self.conf.get('do_this_thing'):
|
||||
conf = self.settings(task, notification)
|
||||
if conf.do_this_thing:
|
||||
# do something with the task and notification
|
||||
|
||||
|
||||
settings.NOTIFICATION_ENGINES.update(
|
||||
{'NewNotificationEngine': NewNotificationEngine})
|
||||
register_notification_handler(NewNotificationHandler)
|
||||
|
||||
They should then be referred to in conf.yaml::
|
||||
You then need to setup the handler to be used either by default for a task,
|
||||
or for a specific task::
|
||||
|
||||
TASK_SETTINGS:
|
||||
signup:
|
||||
workflow:
|
||||
task_defaults:
|
||||
notifications:
|
||||
NewNotificationEngine:
|
||||
standard:
|
||||
do_this_thing: True
|
||||
error:
|
||||
do_this_thing: False
|
||||
standard_handlers:
|
||||
- NewNotificationHandler
|
||||
standard_handler_settings:
|
||||
NewNotificationHandler:
|
||||
do_this_thing: true
|
||||
tasks:
|
||||
some_task:
|
||||
notifications:
|
||||
standard_handlers: null
|
||||
error_handlers:
|
||||
- NewNotificationHandler
|
||||
error_handler_settings:
|
||||
NewNotificationHandler:
|
||||
do_this_thing: true
|
||||
|
||||
|
||||
*************************************************
|
||||
|
793
etc/adjutant.yaml
Normal file
793
etc/adjutant.yaml
Normal file
@ -0,0 +1,793 @@
|
||||
django:
|
||||
# String
|
||||
# The Django secret key.
|
||||
secret_key: Do not ever use this awful secret in prod!!!!
|
||||
# Boolean
|
||||
# Django debug mode is turned on.
|
||||
debug: False
|
||||
# List
|
||||
# The Django allowed hosts
|
||||
allowed_hosts:
|
||||
- '*'
|
||||
# List
|
||||
# A list of additional django apps.
|
||||
# additional_apps:
|
||||
# Dict
|
||||
# Django databases config.
|
||||
databases:
|
||||
default:
|
||||
ATOMIC_REQUESTS: false
|
||||
AUTOCOMMIT: true
|
||||
CONN_MAX_AGE: 0
|
||||
ENGINE: django.db.backends.sqlite3
|
||||
HOST: ''
|
||||
NAME: db.sqlite3
|
||||
OPTIONS: {}
|
||||
PASSWORD: ''
|
||||
PORT: ''
|
||||
TEST:
|
||||
CHARSET: null
|
||||
COLLATION: null
|
||||
MIRROR: null
|
||||
NAME: null
|
||||
TIME_ZONE: null
|
||||
USER: ''
|
||||
# Dict
|
||||
# A full override of the Django logging config for more customised logging.
|
||||
# logging:
|
||||
# String
|
||||
# The name and location of the Adjutant log file, superceded by 'adjutant.django.logging'.
|
||||
log_file: adjutant.log
|
||||
email:
|
||||
# String
|
||||
# Django email backend to use.
|
||||
email_backend: django.core.mail.backends.console.EmailBackend
|
||||
# Integer
|
||||
# Email backend timeout.
|
||||
# timeout: <your_value>
|
||||
# Hostname
|
||||
# Email backend server location.
|
||||
# host: <your_value>
|
||||
# Port
|
||||
# Email backend server port.
|
||||
# port: <your_value>
|
||||
# String
|
||||
# Email backend user.
|
||||
# host_user: <your_value>
|
||||
# String
|
||||
# Email backend user password.
|
||||
# host_password: <your_value>
|
||||
# Boolean
|
||||
# Whether to use TLS for email. Mutually exclusive with 'use_ssl'.
|
||||
use_tls: False
|
||||
# Boolean
|
||||
# Whether to use SSL for email. Mutually exclusive with 'use_tls'.
|
||||
use_ssl: False
|
||||
|
||||
identity:
|
||||
# Integer
|
||||
# Cache time for Keystone Tokens in the Keystone Middleware.
|
||||
token_cache_time: -1
|
||||
# Boolean
|
||||
# Is Adjutant allowed (or able) to edit users in Keystone.
|
||||
can_edit_users: True
|
||||
# Boolean
|
||||
# Should Adjutant assume and treat all usernames as emails.
|
||||
username_is_email: True
|
||||
# Dict
|
||||
# A mapping from held role to roles it is allowed to manage.
|
||||
role_mapping:
|
||||
admin:
|
||||
- project_admin
|
||||
- project_mod
|
||||
- heat_stack_owner
|
||||
- member
|
||||
project_admin:
|
||||
- project_admin
|
||||
- project_mod
|
||||
- heat_stack_owner
|
||||
- member
|
||||
project_mod:
|
||||
- project_mod
|
||||
- heat_stack_owner
|
||||
- member
|
||||
auth:
|
||||
# String
|
||||
# Username for Adjutant Keystone admin user.
|
||||
# username: <your_value>
|
||||
# String
|
||||
# Password for Adjutant Keystone admin user.
|
||||
# password: <your_value>
|
||||
# String
|
||||
# Project name for Adjutant Keystone admin user.
|
||||
# project_name: <your_value>
|
||||
# String
|
||||
# Project domain id for Adjutant Keystone admin user.
|
||||
project_domain_id: default
|
||||
# String
|
||||
# User domain id for Adjutant Keystone admin user.
|
||||
user_domain_id: default
|
||||
# URI
|
||||
# Keystone auth url that Adjutant will use.
|
||||
# auth_url: <your_value>
|
||||
|
||||
api:
|
||||
# List
|
||||
# List of Active Delegate APIs.
|
||||
active_delegate_apis:
|
||||
- UserRoles
|
||||
- UserDetail
|
||||
- UserResetPassword
|
||||
- UserList
|
||||
- RoleList
|
||||
delegate_apis:
|
||||
CreateProjectAndUser:
|
||||
# String
|
||||
# Default region in which any potential resources may be created.
|
||||
default_region: RegionOne
|
||||
# String
|
||||
# Domain in which project and users will be created.
|
||||
default_domain_id: default
|
||||
# String
|
||||
# Parent id under which this project will be created. Default is None, and will create under default domain.
|
||||
# default_parent_id: <your_value>
|
||||
UserList:
|
||||
# List
|
||||
# Users with any of these roles will be hidden from the user list.
|
||||
blacklisted_roles:
|
||||
- admin
|
||||
UserDetail:
|
||||
# List
|
||||
# User with these roles will return not found.
|
||||
blacklisted_roles:
|
||||
- admin
|
||||
UserRoles:
|
||||
# List
|
||||
# User with these roles will return not found.
|
||||
blacklisted_roles:
|
||||
- admin
|
||||
SignUp:
|
||||
# String
|
||||
# Default region in which any potential resources may be created.
|
||||
default_region: RegionOne
|
||||
# String
|
||||
# Domain in which project and users will be created.
|
||||
default_domain_id: default
|
||||
# String
|
||||
# Parent id under which this project will be created. Default is None, and will create under default domain.
|
||||
# default_parent_id: <your_value>
|
||||
|
||||
notifications:
|
||||
handler_defaults:
|
||||
EmailNotification:
|
||||
# List
|
||||
# List of email addresses to send this notification to.
|
||||
# emails:
|
||||
# String
|
||||
# From email for this notification.
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
# String
|
||||
# Reply-to email for this notification.
|
||||
reply: no-reply@example.com
|
||||
# String
|
||||
# Email template for this notification. No template will cause the email not to send.
|
||||
template: notification.txt
|
||||
# String
|
||||
# Email html template for this notification.
|
||||
# html_template: <your_value>
|
||||
|
||||
workflow:
|
||||
# URI
|
||||
# The base Horizon url for Adjutant to use when producing links to Horizon.
|
||||
horizon_url: http://localhost/
|
||||
# Integer
|
||||
# The default token expiry time for Task tokens.
|
||||
default_token_expiry: 86400
|
||||
task_defaults:
|
||||
emails:
|
||||
initial:
|
||||
# String
|
||||
# Default email subject for this stage
|
||||
subject: Task Confirmation
|
||||
# String
|
||||
# Default from email for this stage
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
# String
|
||||
# Default reply-to email for this stage
|
||||
reply: no-reply@example.com
|
||||
# String
|
||||
# Default email template for this stage
|
||||
template: initial.txt
|
||||
# String
|
||||
# Default email html template for this stage
|
||||
# html_template: <your_value>
|
||||
token:
|
||||
# String
|
||||
# Default email subject for this stage
|
||||
subject: Task Token
|
||||
# String
|
||||
# Default from email for this stage
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
# String
|
||||
# Default reply-to email for this stage
|
||||
reply: no-reply@example.com
|
||||
# String
|
||||
# Default email template for this stage
|
||||
template: token.txt
|
||||
# String
|
||||
# Default email html template for this stage
|
||||
# html_template: <your_value>
|
||||
completed:
|
||||
# String
|
||||
# Default email subject for this stage
|
||||
subject: Task Completed
|
||||
# String
|
||||
# Default from email for this stage
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
# String
|
||||
# Default reply-to email for this stage
|
||||
reply: no-reply@example.com
|
||||
# String
|
||||
# Default email template for this stage
|
||||
template: completed.txt
|
||||
# String
|
||||
# Default email html template for this stage
|
||||
# html_template: <your_value>
|
||||
notifications:
|
||||
# List
|
||||
# Handlers to use for standard notifications.
|
||||
standard_handlers:
|
||||
- EmailNotification
|
||||
# List
|
||||
# Handlers to use for error notifications.
|
||||
error_handlers:
|
||||
- EmailNotification
|
||||
# Dict
|
||||
# Settings for standard notification handlers.
|
||||
# standard_handler_config:
|
||||
# Dict
|
||||
# Settings for error notification handlers.
|
||||
# error_handler_config:
|
||||
# List
|
||||
# Error types which are safe to acknowledge automatically.
|
||||
safe_errors:
|
||||
- SMTPException
|
||||
action_defaults:
|
||||
NewProjectWithUserAction:
|
||||
# List
|
||||
# Roles to be given on project for the user.
|
||||
default_roles:
|
||||
- member
|
||||
- project_admin
|
||||
NewProjectAction:
|
||||
# List
|
||||
# Roles to be given on project to the creating user.
|
||||
default_roles:
|
||||
- member
|
||||
- project_admin
|
||||
AddDefaultUsersToProjectAction:
|
||||
# List
|
||||
# Users which this action should add to the project.
|
||||
# default_users:
|
||||
# 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
|
||||
# Name to be given to the default network.
|
||||
network_name: default_network
|
||||
# String
|
||||
# Name to be given to the default subnet.
|
||||
subnet_name: default_subnet
|
||||
# String
|
||||
# Name to be given to the default router.
|
||||
router_name: default_router
|
||||
# String
|
||||
# ID of the public network.
|
||||
# public_network: <your_value>
|
||||
# String
|
||||
# CIDR for the default subnet.
|
||||
# subnet_cidr: <your_value>
|
||||
# List
|
||||
# DNS nameservers for the subnet.
|
||||
# dns_nameservers:
|
||||
# Dict
|
||||
# Specific per region config for default network. See 'region_defaults'.
|
||||
# regions:
|
||||
NewProjectDefaultNetworkAction:
|
||||
region_defaults:
|
||||
# String
|
||||
# Name to be given to the default network.
|
||||
network_name: default_network
|
||||
# String
|
||||
# Name to be given to the default subnet.
|
||||
subnet_name: default_subnet
|
||||
# String
|
||||
# Name to be given to the default router.
|
||||
router_name: default_router
|
||||
# String
|
||||
# ID of the public network.
|
||||
# public_network: <your_value>
|
||||
# String
|
||||
# CIDR for the default subnet.
|
||||
# subnet_cidr: <your_value>
|
||||
# List
|
||||
# DNS nameservers for the subnet.
|
||||
# dns_nameservers:
|
||||
# Dict
|
||||
# Specific per region config for default network. See 'region_defaults'.
|
||||
# regions:
|
||||
SetProjectQuotaAction:
|
||||
# Float
|
||||
# Precentage different allowed when matching quota sizes.
|
||||
size_difference_threshold: 0.1
|
||||
# Integer
|
||||
# The allowed number of days between auto approved quota changes.
|
||||
days_between_autoapprove: 30
|
||||
# Dict
|
||||
# Which quota size to use for which region.
|
||||
region_sizes:
|
||||
RegionOne: small
|
||||
UpdateProjectQuotasAction:
|
||||
# Float
|
||||
# Precentage different allowed when matching quota sizes.
|
||||
size_difference_threshold: 0.1
|
||||
# Integer
|
||||
# The allowed number of days between auto approved quota changes.
|
||||
days_between_autoapprove: 30
|
||||
SendAdditionalEmailAction:
|
||||
prepare:
|
||||
# String
|
||||
# Email subject for this stage.
|
||||
subject: Openstack Email Notification
|
||||
# String
|
||||
# From email for this stage.
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
# String
|
||||
# Reply-to email for this stage.
|
||||
reply: no-reply@example.com
|
||||
# String
|
||||
# Email template for this stage. No template will cause the email not to send.
|
||||
# template: <your_value>
|
||||
# String
|
||||
# Email html template for this stage. No template will cause the email not to send.
|
||||
# html_template: <your_value>
|
||||
# Boolean
|
||||
# Email the user who started the task.
|
||||
email_current_user: False
|
||||
# Boolean
|
||||
# Send to an email set in the task cache.
|
||||
email_task_cache: False
|
||||
# List
|
||||
# Send emails to the given roles on the project.
|
||||
# email_roles:
|
||||
# List
|
||||
# Send emails to an arbitrary admin emails
|
||||
# email_additional_addresses:
|
||||
approve:
|
||||
# String
|
||||
# Email subject for this stage.
|
||||
subject: Openstack Email Notification
|
||||
# String
|
||||
# From email for this stage.
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
# String
|
||||
# Reply-to email for this stage.
|
||||
reply: no-reply@example.com
|
||||
# String
|
||||
# Email template for this stage. No template will cause the email not to send.
|
||||
# template: <your_value>
|
||||
# String
|
||||
# Email html template for this stage. No template will cause the email not to send.
|
||||
# html_template: <your_value>
|
||||
# Boolean
|
||||
# Email the user who started the task.
|
||||
email_current_user: False
|
||||
# Boolean
|
||||
# Send to an email set in the task cache.
|
||||
email_task_cache: False
|
||||
# List
|
||||
# Send emails to the given roles on the project.
|
||||
# email_roles:
|
||||
# List
|
||||
# Send emails to an arbitrary admin emails
|
||||
# email_additional_addresses:
|
||||
submit:
|
||||
# String
|
||||
# Email subject for this stage.
|
||||
subject: Openstack Email Notification
|
||||
# String
|
||||
# From email for this stage.
|
||||
from: bounce+%(task_uuid)s@example.com
|
||||
# String
|
||||
# Reply-to email for this stage.
|
||||
reply: no-reply@example.com
|
||||
# String
|
||||
# Email template for this stage. No template will cause the email not to send.
|
||||
# template: <your_value>
|
||||
# String
|
||||
# Email html template for this stage. No template will cause the email not to send.
|
||||
# html_template: <your_value>
|
||||
# Boolean
|
||||
# Email the user who started the task.
|
||||
email_current_user: False
|
||||
# Boolean
|
||||
# Send to an email set in the task cache.
|
||||
email_task_cache: False
|
||||
# List
|
||||
# Send emails to the given roles on the project.
|
||||
# email_roles:
|
||||
# List
|
||||
# Send emails to an arbitrary admin emails
|
||||
# email_additional_addresses:
|
||||
tasks:
|
||||
create_project_and_user:
|
||||
# Boolean
|
||||
# Override if this task allows auto_approval. Otherwise uses task default.
|
||||
allow_auto_approve: True
|
||||
# List
|
||||
# Additional actions to be run as part of the task after default actions.
|
||||
# additional_actions:
|
||||
# Integer
|
||||
# Override for the task token expiry. Otherwise uses task default.
|
||||
# token_expiry: <your_value>
|
||||
# Dict
|
||||
# Action config overrides over the action defaults. See 'adjutant.workflow.action_defaults'.
|
||||
actions:
|
||||
SomeCustomAction:
|
||||
some_action_setting: <a-uuid-probably>
|
||||
# Dict
|
||||
# Email config overrides for this task over task defaults.See 'adjutant.workflow.emails'.
|
||||
emails:
|
||||
completed:
|
||||
subject: signup completed
|
||||
template: create_project_and_user_completed.txt
|
||||
initial:
|
||||
subject: signup received
|
||||
template: create_project_and_user_initial.txt
|
||||
token:
|
||||
subject: signup approved
|
||||
template: create_project_and_user_token.txt
|
||||
# Dict
|
||||
# Notification config overrides for this task over task defaults.See 'adjutant.workflow.notifications'.
|
||||
notifications:
|
||||
error_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
error_handlers:
|
||||
- EmailNotification
|
||||
standard_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
standard_handlers:
|
||||
- EmailNotification
|
||||
edit_user_roles:
|
||||
# Boolean
|
||||
# Override if this task allows auto_approval. Otherwise uses task default.
|
||||
allow_auto_approve: True
|
||||
# List
|
||||
# Additional actions to be run as part of the task after default actions.
|
||||
# additional_actions:
|
||||
# Integer
|
||||
# Override for the task token expiry. Otherwise uses task default.
|
||||
# token_expiry: <your_value>
|
||||
# Dict
|
||||
# Action config overrides over the action defaults. See 'adjutant.workflow.action_defaults'.
|
||||
actions:
|
||||
SomeCustomAction:
|
||||
some_action_setting: <a-uuid-probably>
|
||||
# Dict
|
||||
# Email config overrides for this task over task defaults.See 'adjutant.workflow.emails'.
|
||||
emails:
|
||||
completed: null
|
||||
initial: null
|
||||
token: null
|
||||
# Dict
|
||||
# Notification config overrides for this task over task defaults.See 'adjutant.workflow.notifications'.
|
||||
notifications:
|
||||
error_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
error_handlers:
|
||||
- EmailNotification
|
||||
standard_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
standard_handlers:
|
||||
- EmailNotification
|
||||
invite_user_to_project:
|
||||
# Boolean
|
||||
# Override if this task allows auto_approval. Otherwise uses task default.
|
||||
allow_auto_approve: True
|
||||
# List
|
||||
# Additional actions to be run as part of the task after default actions.
|
||||
# additional_actions:
|
||||
# Integer
|
||||
# Override for the task token expiry. Otherwise uses task default.
|
||||
# token_expiry: <your_value>
|
||||
# Dict
|
||||
# Action config overrides over the action defaults. See 'adjutant.workflow.action_defaults'.
|
||||
actions:
|
||||
SomeCustomAction:
|
||||
some_action_setting: <a-uuid-probably>
|
||||
# Dict
|
||||
# Email config overrides for this task over task defaults.See 'adjutant.workflow.emails'.
|
||||
emails:
|
||||
completed:
|
||||
subject: invite_user_to_project
|
||||
template: invite_user_to_project_completed.txt
|
||||
initial: null
|
||||
token:
|
||||
subject: invite_user_to_project
|
||||
template: invite_user_to_project_token.txt
|
||||
# Dict
|
||||
# Notification config overrides for this task over task defaults.See 'adjutant.workflow.notifications'.
|
||||
notifications:
|
||||
error_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
error_handlers:
|
||||
- EmailNotification
|
||||
standard_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
standard_handlers:
|
||||
- EmailNotification
|
||||
reset_user_password:
|
||||
# Boolean
|
||||
# Override if this task allows auto_approval. Otherwise uses task default.
|
||||
allow_auto_approve: True
|
||||
# List
|
||||
# Additional actions to be run as part of the task after default actions.
|
||||
# additional_actions:
|
||||
# Integer
|
||||
# Override for the task token expiry. Otherwise uses task default.
|
||||
# token_expiry: <your_value>
|
||||
# Dict
|
||||
# Action config overrides over the action defaults. See 'adjutant.workflow.action_defaults'.
|
||||
actions:
|
||||
SomeCustomAction:
|
||||
some_action_setting: <a-uuid-probably>
|
||||
# Dict
|
||||
# Email config overrides for this task over task defaults.See 'adjutant.workflow.emails'.
|
||||
emails:
|
||||
completed:
|
||||
subject: Password Reset for OpenStack
|
||||
template: reset_user_password_completed.txt
|
||||
initial: null
|
||||
token:
|
||||
subject: Password Reset for OpenStack
|
||||
template: reset_user_password_token.txt
|
||||
# Dict
|
||||
# Notification config overrides for this task over task defaults.See 'adjutant.workflow.notifications'.
|
||||
notifications:
|
||||
error_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
error_handlers:
|
||||
- EmailNotification
|
||||
standard_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
standard_handlers:
|
||||
- EmailNotification
|
||||
update_user_email:
|
||||
# Boolean
|
||||
# Override if this task allows auto_approval. Otherwise uses task default.
|
||||
allow_auto_approve: True
|
||||
# List
|
||||
# Additional actions to be run as part of the task after default actions.
|
||||
additional_actions:
|
||||
- SendAdditionalEmailAction
|
||||
# Integer
|
||||
# Override for the task token expiry. Otherwise uses task default.
|
||||
# token_expiry: <your_value>
|
||||
# Dict
|
||||
# Action config overrides over the action defaults. See 'adjutant.workflow.action_defaults'.
|
||||
actions:
|
||||
SendAdditionalEmailAction:
|
||||
initial:
|
||||
email_current_user: true
|
||||
subject: OpenStack Email Update Requested
|
||||
template: update_user_email_started.txt
|
||||
# Dict
|
||||
# Email config overrides for this task over task defaults.See 'adjutant.workflow.emails'.
|
||||
emails:
|
||||
completed:
|
||||
subject: Email Update Complete
|
||||
template: update_user_email_completed.txt
|
||||
initial: null
|
||||
token:
|
||||
subject: update_user_email_token
|
||||
template: update_user_email_token.txt
|
||||
# Dict
|
||||
# Notification config overrides for this task over task defaults.See 'adjutant.workflow.notifications'.
|
||||
notifications:
|
||||
error_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
error_handlers:
|
||||
- EmailNotification
|
||||
standard_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
standard_handlers:
|
||||
- EmailNotification
|
||||
update_quota:
|
||||
# Boolean
|
||||
# Override if this task allows auto_approval. Otherwise uses task default.
|
||||
allow_auto_approve: True
|
||||
# List
|
||||
# Additional actions to be run as part of the task after default actions.
|
||||
# additional_actions:
|
||||
# Integer
|
||||
# Override for the task token expiry. Otherwise uses task default.
|
||||
# token_expiry: <your_value>
|
||||
# Dict
|
||||
# Action config overrides over the action defaults. See 'adjutant.workflow.action_defaults'.
|
||||
actions:
|
||||
SomeCustomAction:
|
||||
some_action_setting: <a-uuid-probably>
|
||||
# Dict
|
||||
# Email config overrides for this task over task defaults.See 'adjutant.workflow.emails'.
|
||||
emails:
|
||||
completed:
|
||||
subject: signup completed
|
||||
template: create_project_and_user_completed.txt
|
||||
initial: null
|
||||
token: null
|
||||
# Dict
|
||||
# Notification config overrides for this task over task defaults.See 'adjutant.workflow.notifications'.
|
||||
notifications:
|
||||
error_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
error_handlers:
|
||||
- EmailNotification
|
||||
standard_handler_config:
|
||||
EmailNotification:
|
||||
emails:
|
||||
- example@example.com
|
||||
reply: no-reply@example.com
|
||||
standard_handlers:
|
||||
- EmailNotification
|
||||
|
||||
quota:
|
||||
# Dict
|
||||
# A definition of the quota size groups that Adjutant should use.
|
||||
sizes:
|
||||
large:
|
||||
cinder:
|
||||
gigabytes: 50000
|
||||
snapshots: 600
|
||||
volumes: 200
|
||||
neutron:
|
||||
floatingip: 50
|
||||
network: 10
|
||||
port: 500
|
||||
router: 10
|
||||
security_group: 100
|
||||
security_group_rule: 800
|
||||
subnet: 10
|
||||
nova:
|
||||
cores: 200
|
||||
fixed_ips: 0
|
||||
floating_ips: 50
|
||||
injected_file_content_bytes: 10240
|
||||
injected_files: 5
|
||||
instances: 100
|
||||
key_pairs: 50
|
||||
metadata_items: 128
|
||||
ram: 655360
|
||||
security_group_rules: 800
|
||||
security_groups: 100
|
||||
octavia:
|
||||
health_monitor: 100
|
||||
listener: 10
|
||||
load_balancer: 10
|
||||
member: 10
|
||||
pool: 10
|
||||
medium:
|
||||
cinder:
|
||||
gigabytes: 10000
|
||||
snapshots: 300
|
||||
volumes: 100
|
||||
neutron:
|
||||
floatingip: 25
|
||||
network: 5
|
||||
port: 250
|
||||
router: 5
|
||||
security_group: 50
|
||||
security_group_rule: 400
|
||||
subnet: 5
|
||||
nova:
|
||||
cores: 100
|
||||
fixed_ips: 0
|
||||
floating_ips: 25
|
||||
injected_file_content_bytes: 10240
|
||||
injected_files: 5
|
||||
instances: 50
|
||||
key_pairs: 50
|
||||
metadata_items: 128
|
||||
ram: 327680
|
||||
security_group_rules: 400
|
||||
security_groups: 50
|
||||
octavia:
|
||||
health_monitor: 50
|
||||
listener: 5
|
||||
load_balancer: 5
|
||||
member: 5
|
||||
pool: 5
|
||||
small:
|
||||
cinder:
|
||||
gigabytes: 5000
|
||||
snapshots: 50
|
||||
volumes: 20
|
||||
neutron:
|
||||
floatingip: 10
|
||||
network: 3
|
||||
port: 50
|
||||
router: 3
|
||||
security_group: 20
|
||||
security_group_rule: 100
|
||||
subnet: 3
|
||||
nova:
|
||||
cores: 20
|
||||
fixed_ips: 0
|
||||
floating_ips: 10
|
||||
injected_file_content_bytes: 10240
|
||||
injected_files: 5
|
||||
instances: 10
|
||||
key_pairs: 50
|
||||
metadata_items: 128
|
||||
ram: 65536
|
||||
security_group_rules: 100
|
||||
security_groups: 20
|
||||
octavia:
|
||||
health_monitor: 5
|
||||
listener: 1
|
||||
load_balancer: 1
|
||||
member: 2
|
||||
pool: 1
|
||||
# List
|
||||
# An ascending list of all the quota size names, so that Adjutant knows their relative sizes/order.
|
||||
sizes_ascending:
|
||||
- small
|
||||
- medium
|
||||
- large
|
||||
# Dict
|
||||
# A per region definition of what services Adjutant should manage quotas for. '*' means all or default region.
|
||||
services:
|
||||
'*':
|
||||
- cinder
|
||||
- neutron
|
||||
- nova
|
||||
|
13
releasenotes/notes/story-2004488-5468c184cc3a4691.yaml
Normal file
13
releasenotes/notes/story-2004488-5468c184cc3a4691.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adjutant's config system is now built on top of CONFspirator, which is
|
||||
a config definition library like oslo.config but tailored specifically
|
||||
for some use-cases that Adjutant has.
|
||||
upgrade:
|
||||
- |
|
||||
An almost entirely different config format will need to be used, but
|
||||
there will be a better feedback from the service during startup
|
||||
regarding the validity of the config. An example is present in
|
||||
`etc/adjutant.yaml` but a new one can be generated by using
|
||||
`tox -e venv adjutant-api exampleconfig`.
|
@ -14,6 +14,7 @@ python-novaclient>=14.0.0
|
||||
python-octaviaclient>=1.8.0
|
||||
PyYAML>=5.1
|
||||
six>=1.12.0
|
||||
confspirator>=0.1.6
|
||||
|
||||
# Address soon:
|
||||
Django>=1.11,<1.12
|
||||
|
@ -8,3 +8,4 @@ coverage>=4.5.3 # Apache-2.0
|
||||
doc8>=0.8.0 # Apache-2.0
|
||||
mock>=3.0.0 # BSD
|
||||
Pygments>=2.2.0 # BSD license
|
||||
flake8-bugbear>=19.3.0;python_version>='3.4' # MIT
|
||||
|
4
tox.ini
4
tox.ini
@ -50,7 +50,9 @@ deps = {[testenv:docs]deps}
|
||||
commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
||||
|
||||
[flake8]
|
||||
ignore = D100,D101,D102,D103,D104,D105,D200,D203,D202,D204,D205,D208,D400,D401,W503
|
||||
max-line-length = 88
|
||||
select = C,E,F,W,B,B950
|
||||
ignore = D100,D101,D102,D103,D104,D105,D200,D203,D202,D204,D205,D208,D400,D401,W503,E501
|
||||
show-source = true
|
||||
builtins = _
|
||||
exclude=.venv,venv,.env,env,.git,.tox,dist,doc,*lib/python*,*egg,releasenotes,adjutant/api/migrations/*,adjutant/actions/migrations,adjutant/tasks/migrations
|
||||
|
Loading…
Reference in New Issue
Block a user