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:
Adrian Turjak 2019-07-09 19:09:51 +12:00 committed by Adrian Turjak
parent c9038dfe69
commit c750fd6d6c
73 changed files with 3808 additions and 2689 deletions

View File

@ -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
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)

View File

@ -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 = {}

View File

@ -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)

View File

@ -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,23 +87,19 @@ 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":
handlers=False)
notification.acknowledged = True
notification.save()
else:

View File

@ -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'],
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:

View File

@ -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'):

View File

@ -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:

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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,17 +82,16 @@ class MiscActionTests(AdjutantTestCase):
@mock.patch('adjutant.actions.utils.EmailMultiAlternatives',
FailEmail)
@modify_dict_settings(DEFAULT_ACTION_SETTINGS={
'operation': 'update',
'key_list': ['SendAdditionalEmailAction', 'token'],
'value': {
@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'
}
}, DEFAULT_TASK_SETTINGS={
'operation': 'delete',
'key_list': ['notifications'],
}},
],
})
def test_send_additional_email_fail(self):
"""
@ -102,7 +104,6 @@ class MiscActionTests(AdjutantTestCase):
task_type='edit_roles',
)
# setup settings
action = SendAdditionalEmailAction({}, task=task, order=1)
action.prepare()
@ -116,20 +117,22 @@ 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': {
@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):
"""
@ -141,7 +144,6 @@ class MiscActionTests(AdjutantTestCase):
keystone_user={}
)
# setup settings
action = SendAdditionalEmailAction({}, task=task, order=1)
action.prepare()
@ -161,14 +163,16 @@ 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': {
@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):
"""
@ -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'],
@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_settings(self):
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()

View File

@ -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({})

View File

@ -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)

View File

@ -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.

View File

@ -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.'

View File

@ -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 = {}

View File

@ -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')

View File

@ -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__)

View File

@ -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(

View File

@ -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})

View File

@ -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)

View File

@ -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,9 +453,12 @@ 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):
"""
@ -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,9 +529,12 @@ 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):
"""
@ -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,9 +569,12 @@ 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):
"""
@ -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': {
@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',
'template': 'notification.txt'
}
},
'error': {
"error_handler_config": {
"EmailNotification": {
'emails': ['example@example.com'],
'reply': 'no-reply@example.com',
'template': 'notification.txt'
}
}
}
}, TASK_SETTINGS={
'key_list': ['create_project_and_user', 'emails'],
'operation': 'override',
'value': {
},
}},
],
"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,11 +1496,12 @@ 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):
"""
@ -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

View File

@ -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,11 +777,13 @@ 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': {
@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,
@ -788,8 +794,12 @@ class QuotaAPITests(AdjutantAPITestCase):
'floatingip': 0, 'network': 0, 'port': 0, 'router': 0,
'security_group': 0, 'security_group_rule': 0}
}
}])
@modify_settings(QUOTA_SIZES_ASC={'prepend': 'zero'})
}},
],
"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,10 +1258,13 @@ 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

View File

@ -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': {
@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': {
@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}
'email_current_user': True,
}
])
@override_settings(USERNAME_IS_EMAIL=False)
}
}},
],
"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',
@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'],
'email_current_user': False,
}
}
])
}},
],
})
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',
@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'],
'email_current_user': False,
}
}
])
}},
],
})
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':{
@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'],
'email_current_user': False,
}
}
])
}},
],
})
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':{
@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'],
'email_current_user': False,
}
}
])
}},
],
})
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,

View File

@ -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())

View File

@ -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

View File

View File

View 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")

View File

@ -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-.]+$)"

View File

@ -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)

View File

@ -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,8 +180,7 @@ 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:
@ -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:

View File

@ -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):

View File

@ -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')

View File

@ -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()

View File

@ -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
View 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
View 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
View 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
View 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)

View 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
View 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
View 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
View 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)

View File

@ -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 = {}

View File

@ -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()
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)

View File

@ -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)

View 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
View 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)

View File

@ -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

View File

@ -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(

View File

@ -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 = {}

View File

@ -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):

View File

@ -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.
"""
if self._config is None:
try:
return settings.TASK_SETTINGS[self.task_type]
task_conf = CONF.workflow.tasks[self.task_type]
except KeyError:
return settings.DEFAULT_TASK_SETTINGS
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):

View File

@ -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:

View File

@ -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)

View File

@ -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'
}
}

View File

@ -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'
}
}

View File

@ -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'
}
}

View File

@ -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
tokenurl = CONF.workflow.horizon_url
if not tokenurl.endswith('/'):
tokenurl += '/'
tokenurl += 'token/'
else:
tokenurl = settings.TOKEN_SUBMISSION_URL
if not tokenurl.endswith('/'):
tokenurl += '/'
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,23 +141,19 @@ 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":
handlers=False)
notification.acknowledged = True
notification.save()
else:

View File

@ -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,
}

View File

@ -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

View File

@ -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)

View File

@ -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"
},
]
}

View File

@ -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

View File

@ -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.

View File

@ -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,7 +77,7 @@ 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)
@ -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
View 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

View 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`.

View File

@ -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

View File

@ -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

View File

@ -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