Major refactor of the Adjutant task layer

This patch splits out the Task layer and the API
layer. This will better allow us to build more logic
into the task layer, and better allow the APIs to be
more flexible.

This sets the foundations for future additions to task
definitions, async task processing, and an overhaul of
the config system for the service.

- Task model and logic moved to 'tasks' app
- TaskViews are now DelegateAPIs
- stage email templates have been moved to the tasks app
- better define Task model indexes
- rename task/action stage pre_approve to prepare
- rename task/action stage post_approve to approve
- Added new TaskManager class for handling tasks
- Removed redundant ip_address value on Task model
- Remove redundant UserSetPassword view
- Added custom exception handling for the API
- Add new exception types
- Simplified error responses by raising exceptions
- standardized task API response codes on 202 unless
  task is completed
- Use 503 Service Unavailable for service issues
- Various task_types changed:
    - create_project to create_project_and_user
    - invite_user to invite_user_to_project
    - reset_password to reset_user_password
    - edit_user to edit_user_roles
    - update_email to update_user_email
- reissuing task token now deletes old task tokens

Story: 2004489

Change-Id: I33381c1c65b28b69f6ffeb3d73b50be95ee30ba7
This commit is contained in:
Adrian Turjak 2019-06-21 17:34:54 +12:00
parent d62eada126
commit c9038dfe69
79 changed files with 2351 additions and 2011 deletions

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.21 on 2019-06-10 02:09
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tasks', '0001_initial'),
('actions', '0003_auto_20190610_0205'),
]
operations = [
migrations.AlterField(
model_name='action',
name='task',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.Task'),
),
]

View File

@ -29,7 +29,7 @@ class Action(models.Model):
state = models.CharField(max_length=200, default="default")
valid = models.BooleanField(default=False)
need_token = models.BooleanField(default=False)
task = models.ForeignKey('api.Task')
task = models.ForeignKey('tasks.Task')
# NOTE(amelia): Auto approve is technically a ternary operator
# If all in a task are None it will not auto approve
# However if at least one action has it set to True it

View File

@ -37,22 +37,22 @@ class BaseAction(object):
The Action can do anything it needs at one of the three functions
called by the views:
- 'pre_approve'
- 'post_approve'
- 'prepare'
- 'approve'
- 'submit'
All logic and validation should be handled within the action itself,
and any other actions it is linked to. The way in which pre_approve,
post_approve, and submit are called should rarely change. Actions
should be built with those steps in mind, thinking about what they mean,
and any other actions it is linked to. The way in which prepare,
approve, and submit are called should rarely change. Actions should
be built with those steps in mind, thinking about what they mean,
and when they execute.
By using 'get_cache' and 'set_cache' they can pass data along which
may be needed by the action later. This cache is backed to the database.
Passing data along to other actions is done via the task and
it's cache, but this is in memory only, so it is only useful during the
same action stage ('post_approve', etc.).
its cache, but this is in memory only, so it is only useful during the
same action stage ('prepare', 'approve', etc.).
Other than the task cache, actions should not be altering database
models other than themselves. This is not enforced, just a guideline.
@ -121,22 +121,17 @@ class BaseAction(object):
return self.action.auto_approve
def set_auto_approve(self, can_approve=True):
task_conf = settings.TASK_SETTINGS.get(self.action.task.task_type, {})
if task_conf.get('allow_auto_approve', True):
self.add_note("Auto approve set to %s." % can_approve)
self.action.auto_approve = can_approve
self.action.save()
else:
self.add_note("Task disallows action auto approve.")
self.action.auto_approve = False
self.action.save()
self.add_note("Auto approve set to %s." % can_approve)
self.action.auto_approve = can_approve
self.action.save()
def add_note(self, note):
"""
Logs the note, and also adds it to the task action notes.
"""
self.logger.info("(%s) - %s" % (timezone.now(), note))
note = "%s - (%s)" % (note, timezone.now())
now = timezone.now()
self.logger.info("(%s) - %s" % (now, note))
note = "%s - (%s)" % (note, now)
self.action.task.add_action_note(
str(self), note)
@ -154,19 +149,31 @@ class BaseAction(object):
return settings.DEFAULT_ACTION_SETTINGS.get(
self.__class__.__name__, {})
def pre_approve(self):
return self._pre_approve()
def prepare(self):
try:
return self._prepare()
except NotImplementedError:
self.logger.warning(
"DEPRECATED: Action '_pre_approve' stage has been renamed "
"to 'prepare'.")
return self._pre_approve()
def post_approve(self):
return self._post_approve()
def approve(self):
try:
return self._approve()
except NotImplementedError:
self.logger.warning(
"DEPRECATED: Action '_post_approve' stage has been renamed "
"to 'prepare'.")
return self._post_approve()
def submit(self, token_data):
return self._submit(token_data)
def _pre_approve(self):
def _prepare(self):
raise NotImplementedError
def _post_approve(self):
def _approve(self):
raise NotImplementedError
def _submit(self, token_data):
@ -265,7 +272,12 @@ class UserMixin(ResourceMixin):
return False
return True
def are_roles_managable(self, user_roles=[], requested_roles=[]):
def are_roles_managable(self, user_roles=None, requested_roles=None):
if user_roles is None:
user_roles = []
if requested_roles is None:
requested_roles = []
requested_roles = set(requested_roles)
# blacklist checks
blacklist_roles = set(['admin'])

View File

@ -68,10 +68,10 @@ class SendAdditionalEmailAction(BaseAction):
self.action.valid = True
self.action.save()
def _pre_approve(self):
def _prepare(self):
self.perform_action('initial')
def _post_approve(self):
def _approve(self):
self.perform_action('token')
def _submit(self, data):

View File

@ -14,7 +14,10 @@
from django.conf import settings
from rest_framework import serializers as drf_serializers
from adjutant.actions.v1 import serializers
from adjutant.actions.v1.base import BaseAction
from adjutant.actions.v1.projects import (
NewProjectWithUserAction, NewProjectAction,
AddDefaultUsersToProjectAction)
@ -25,11 +28,23 @@ from adjutant.actions.v1.resources import (
NewDefaultNetworkAction, NewProjectDefaultNetworkAction,
SetProjectQuotaAction, UpdateProjectQuotasAction)
from adjutant.actions.v1.misc import SendAdditionalEmailAction
from adjutant import exceptions
# Update settings dict with tuples in the format:
# (<ActionClass>, <ActionSerializer>)
def register_action_class(action_class, serializer_class):
if not issubclass(action_class, BaseAction):
raise exceptions.InvalidActionClass(
"'%s' is not a built off the BaseAction class."
% action_class.__name__
)
if serializer_class and not issubclass(
serializer_class, drf_serializers.Serializer):
raise exceptions.InvalidActionSerializer(
"serializer for '%s' is not a valid DRF serializer."
% action_class.__name__
)
data = {}
data[action_class.__name__] = (action_class, serializer_class)
settings.ACTION_CLASSES.update(data)

View File

@ -28,7 +28,7 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
"""
Creates a new project for the current keystone_user.
This action can only be used for an autheticated taskview.
This action can only be used for an autheticated task.
"""
required = [
@ -69,10 +69,10 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
return self._validate_parent_project()
return True
def _pre_approve(self):
def _prepare(self):
self._validate()
def _post_approve(self):
def _approve(self):
project_id = self.get_cache('project_id')
if project_id:
self.action.task.cache['project_id'] = project_id
@ -116,7 +116,7 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
def _submit(self, token_data):
"""
Nothing to do here. Everything is done at post_approve.
Nothing to do here. Everything is done at the approve step.
"""
pass
@ -215,10 +215,10 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
self.action.save()
def _pre_approve(self):
def _prepare(self):
self._validate()
def _post_approve(self):
def _approve(self):
"""
Approving a new project means we set up the project itself,
and if the user doesn't exist, create it right away. An existing
@ -369,7 +369,7 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
The submit action is performed when a token is submitted.
This is done to set a user password only, and so should now only
change the user password. The project and user themselves are created
on post_approve.
on approve.
"""
self._validate_user_submit()
@ -445,10 +445,10 @@ class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
])
self.action.save()
def _pre_approve(self):
def _prepare(self):
self._pre_validate()
def _post_approve(self):
def _approve(self):
id_manager = user_store.IdentityManager()
self.project_id = self.action.task.cache.get('project_id', None)
self._validate()

View File

@ -165,12 +165,12 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
self.add_note(
"Interface added to router for project %s" % self.project_id)
def _pre_approve(self):
def _prepare(self):
# Note: Do we need to get this from cache? it is a required setting
# self.project_id = self.action.task.cache.get('project_id', None)
self._validate()
def _post_approve(self):
def _approve(self):
self._validate()
if self.setup_network and self.valid:
@ -183,7 +183,7 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
class NewProjectDefaultNetworkAction(NewDefaultNetworkAction):
"""
A variant of NewDefaultNetwork that expects the project
to not be created until after post_approve.
to not be created until after approve.
"""
required = [
@ -207,10 +207,10 @@ class NewProjectDefaultNetworkAction(NewDefaultNetworkAction):
])
self.action.save()
def _pre_approve(self):
def _prepare(self):
self._pre_validate()
def _post_approve(self):
def _approve(self):
self.project_id = self.action.task.cache.get('project_id', None)
self._validate()
@ -349,12 +349,12 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
])
self.action.save()
def _pre_approve(self):
def _prepare(self):
self._validate()
# Set auto-approval
self.set_auto_approve(self._can_auto_approve())
def _post_approve(self):
def _approve(self):
self._validate()
if not self.valid or self.action.state == "completed":
@ -373,7 +373,7 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
def _submit(self, token_data):
"""
Nothing to do here. Everything is done at post_approve.
Nothing to do here. Everything is done at approve.
"""
pass
@ -392,12 +392,12 @@ class SetProjectQuotaAction(UpdateProjectQuotasAction):
])
self.action.save()
def _pre_approve(self):
def _prepare(self):
# Nothing to validate yet
self.action.valid = True
self.action.save()
def _post_approve(self):
def _approve(self):
# Assumption: another action has placed the project_id into the cache.
self.project_id = self.action.task.cache.get('project_id', None)
self._validate()

View File

@ -46,7 +46,6 @@ class MiscActionTests(AdjutantTestCase):
to_address = "test@example.com"
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -66,7 +65,6 @@ class MiscActionTests(AdjutantTestCase):
to_address = []
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -100,7 +98,6 @@ class MiscActionTests(AdjutantTestCase):
"""
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={},
task_type='edit_roles',
)
@ -108,13 +105,13 @@ class MiscActionTests(AdjutantTestCase):
# setup settings
action = SendAdditionalEmailAction({}, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
task.cache["additional_emails"] = ["thisguy@righthere.com",
"nope@example.com"]
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
self.assertEqual(len(mail.outbox), 0)
@ -141,20 +138,19 @@ class MiscActionTests(AdjutantTestCase):
"""
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
# setup settings
action = SendAdditionalEmailAction({}, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
task.cache["additional_emails"] = ["thisguy@righthere.com",
"nope@example.com"]
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
self.assertEqual(len(mail.outbox), 1)
@ -181,17 +177,16 @@ class MiscActionTests(AdjutantTestCase):
"""
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
# setup settings
action = SendAdditionalEmailAction({}, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
self.assertEqual(len(mail.outbox), 0)
@ -215,17 +210,16 @@ class MiscActionTests(AdjutantTestCase):
"""
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
# setup settings
action = SendAdditionalEmailAction({}, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
self.assertEqual(len(mail.outbox), 1)

View File

@ -35,14 +35,13 @@ class ProjectActionTests(TestCase):
"""
Base case, no project, no user.
Project and user created at post_approve step,
Project and user created at approve step,
user password at submit step.
"""
setup_identity_cache()
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -55,10 +54,10 @@ class ProjectActionTests(TestCase):
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
new_project = fake_clients.identity_cache['new_projects'][0]
@ -88,14 +87,13 @@ class ProjectActionTests(TestCase):
def test_new_project_reapprove(self):
"""
Project created at post_approve step,
Project created at approve step,
ensure reapprove does nothing.
"""
setup_identity_cache()
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -108,10 +106,10 @@ class ProjectActionTests(TestCase):
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
new_project = fake_clients.identity_cache['new_projects'][0]
@ -126,7 +124,7 @@ class ProjectActionTests(TestCase):
{'project_id': new_project.id, 'user_id': new_user.id,
'user_state': 'default'})
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
self.assertEqual(
len(fake_clients.identity_cache['new_projects']), 1)
@ -152,7 +150,7 @@ class ProjectActionTests(TestCase):
def test_new_project_reapprove_failure(self):
"""
Project created at post_approve step, failure at role grant.
Project created at approve step, failure at role grant.
Ensure reapprove correctly finishes.
"""
@ -160,7 +158,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache()
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -173,7 +170,7 @@ class ProjectActionTests(TestCase):
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
# NOTE(adrian): We need the code to fail at the
@ -189,7 +186,7 @@ class ProjectActionTests(TestCase):
action.grant_roles = fail_grant
# Now we expect the failure
self.assertRaises(FakeException, action.post_approve)
self.assertRaises(FakeException, action.approve)
# No roles_granted yet, but user created
self.assertTrue("user_id" in action.action.cache)
@ -206,7 +203,7 @@ class ProjectActionTests(TestCase):
# And then swap back the correct function
action.grant_roles = old_grant_function
# and try again, it should work this time
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
# roles_granted in cache
self.assertTrue("roles_granted" in action.action.cache)
@ -235,7 +232,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache(users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -248,10 +244,10 @@ class ProjectActionTests(TestCase):
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
new_project = fake_clients.identity_cache['new_projects'][0]
@ -291,7 +287,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache(users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -305,10 +300,10 @@ class ProjectActionTests(TestCase):
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, False)
action.post_approve()
action.approve()
self.assertEqual(action.valid, False)
self.assertEqual(
@ -327,7 +322,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache()
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -340,10 +334,10 @@ class ProjectActionTests(TestCase):
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
new_project = fake_clients.identity_cache['new_projects'][0]
@ -363,7 +357,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache()
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -376,10 +369,10 @@ class ProjectActionTests(TestCase):
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
new_user = fake_clients.identity_cache['new_users'][0]
@ -404,7 +397,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache(users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -418,10 +410,10 @@ class ProjectActionTests(TestCase):
# Sign up, approve
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
new_project = fake_clients.identity_cache['new_projects'][0]
@ -471,7 +463,6 @@ class ProjectActionTests(TestCase):
# Sign up for the project+user, validate.
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -484,7 +475,7 @@ class ProjectActionTests(TestCase):
# Sign up
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
# Create the disabled user directly with the Identity Manager.
@ -500,7 +491,7 @@ class ProjectActionTests(TestCase):
fake_client.disable_user(user.id)
# approve previous signup
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
new_project = fake_clients.identity_cache['new_projects'][0]
@ -544,7 +535,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache(projects=[project])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id',
@ -560,10 +550,10 @@ class ProjectActionTests(TestCase):
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, False)
action.post_approve()
action.approve()
self.assertEqual(action.valid, False)
def test_new_project_invalid_domain_id(self):
@ -572,7 +562,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache()
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id',
@ -588,10 +577,10 @@ class ProjectActionTests(TestCase):
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, False)
action.post_approve()
action.approve()
self.assertEqual(action.valid, False)
@override_settings(USERNAME_IS_EMAIL=False)
@ -599,14 +588,13 @@ class ProjectActionTests(TestCase):
"""
Base case, no project, no user.
Project and user created at post_approve step,
Project and user created at approve step,
user password at submit step.
"""
setup_identity_cache()
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={}
)
@ -620,10 +608,10 @@ class ProjectActionTests(TestCase):
action = NewProjectWithUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
new_project = fake_clients.identity_cache['new_projects'][0]
@ -670,17 +658,17 @@ class ProjectActionTests(TestCase):
setup_identity_cache(projects=[project])
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
task.cache = {'project_id': project.id}
action = AddDefaultUsersToProjectAction(
{'domain_id': 'default'}, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
fake_client = fake_clients.FakeManager()
@ -691,23 +679,23 @@ class ProjectActionTests(TestCase):
def test_add_default_users_invalid_project(self):
"""Add default users to a project that doesn't exist.
Action should become invalid at the post_approve state, it's ok if
the project isn't created yet during pre_approve.
Action should become invalid at the approve state, it's ok if
the project isn't created yet during prepare.
"""
setup_identity_cache()
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
task.cache = {'project_id': "invalid_project_id"}
action = AddDefaultUsersToProjectAction(
{'domain_id': 'default'}, task=task, order=1)
action.pre_approve()
action.prepare()
# No need to test project yet - it's ok if it doesn't exist
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
# Now the missing project should make the action invalid
self.assertEqual(action.valid, False)
@ -725,17 +713,17 @@ class ProjectActionTests(TestCase):
setup_identity_cache(projects=[project])
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
task.cache = {'project_id': project.id}
action = AddDefaultUsersToProjectAction(
{'domain_id': 'default'}, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
fake_client = fake_clients.FakeManager()
@ -743,7 +731,7 @@ class ProjectActionTests(TestCase):
roles = fake_client._get_roles_as_names(user, project)
self.assertEqual(roles, ['admin'])
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
roles = fake_client._get_roles_as_names(user, project)
@ -762,7 +750,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache(projects=[project], users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={"user_id": user.id,
"project_id": project.id,
"project_domain_id": 'default'})
@ -776,10 +763,10 @@ class ProjectActionTests(TestCase):
action = NewProjectAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
new_project = fake_clients.identity_cache['new_projects'][0]
@ -796,7 +783,7 @@ class ProjectActionTests(TestCase):
action.submit({})
self.assertEqual(action.valid, True)
def test_new_project_action_rerun_post_approve(self):
def test_new_project_action_rerun_approve(self):
"""
Tests the new project action for an existing user does
nothing on reapproval.
@ -810,7 +797,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache(projects=[project], users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={"user_id": user.id,
"project_id": project.id,
"project_domain_id": 'default'})
@ -824,10 +810,10 @@ class ProjectActionTests(TestCase):
action = NewProjectAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
new_project = fake_clients.identity_cache['new_projects'][0]
@ -841,7 +827,7 @@ class ProjectActionTests(TestCase):
sorted(['_member_', 'project_admin',
'project_mod', 'heat_stack_owner']))
action.post_approve()
action.approve()
# Nothing should change
self.assertEqual(action.valid, True)
@ -871,7 +857,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache(projects=[project], users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={"user_id": user.id,
"project_id": project.id,
"project_domain_id": 'default'})
@ -885,10 +870,10 @@ class ProjectActionTests(TestCase):
action = NewProjectAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, False)
action.post_approve()
action.approve()
self.assertEqual(action.valid, False)
action.submit({})
@ -908,7 +893,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache(projects=[project], users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={"user_id": user.id,
"project_id": project.id,
"project_domain_id": 'default'})
@ -922,10 +906,10 @@ class ProjectActionTests(TestCase):
action = NewProjectAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, False)
action.post_approve()
action.approve()
self.assertEqual(action.valid, False)
action.submit({})
@ -944,7 +928,6 @@ class ProjectActionTests(TestCase):
setup_identity_cache(projects=[project], users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={"user_id": user.id,
"project_id": project.id,
"project_domain_id": 'default'})
@ -958,10 +941,10 @@ class ProjectActionTests(TestCase):
action = NewProjectAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
new_project = fake_clients.identity_cache['new_projects'][0]

View File

@ -50,7 +50,6 @@ class ProjectSetupActionTests(TestCase):
"""
setup_neutron_cache('RegionOne', 'test_project_id')
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin'],
'project_id': 'test_project_id'})
@ -72,10 +71,10 @@ class ProjectSetupActionTests(TestCase):
action = NewDefaultNetworkAction(
data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
self.assertEqual(
@ -100,7 +99,6 @@ class ProjectSetupActionTests(TestCase):
"""
setup_neutron_cache('RegionOne', 'test_project_id')
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin'],
'project_id': 'test_project_id'})
@ -122,10 +120,10 @@ class ProjectSetupActionTests(TestCase):
action = NewDefaultNetworkAction(
data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
self.assertEqual(action.action.cache, {})
@ -145,7 +143,6 @@ class ProjectSetupActionTests(TestCase):
setup_neutron_cache('RegionOne', 'test_project_id')
global neutron_cache
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin'],
'project_id': 'test_project_id'})
@ -167,13 +164,13 @@ class ProjectSetupActionTests(TestCase):
action = NewDefaultNetworkAction(
data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
neutron_cache['RegionOne']['test_project_id']['routers'] = []
try:
action.post_approve()
action.approve()
self.fail("Shouldn't get here.")
except Exception:
pass
@ -193,7 +190,7 @@ class ProjectSetupActionTests(TestCase):
neutron_cache['RegionOne']['test_project_id']['routers'] = {}
action.post_approve()
action.approve()
self.assertEqual(
action.action.cache,
@ -227,7 +224,7 @@ class ProjectSetupActionTests(TestCase):
"""
setup_neutron_cache('RegionOne', 'test_project_id')
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
data = {
'setup_network': True,
@ -237,7 +234,7 @@ class ProjectSetupActionTests(TestCase):
action = NewProjectDefaultNetworkAction(
data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
# Now we add the project data as this is where the project
@ -252,7 +249,7 @@ class ProjectSetupActionTests(TestCase):
task.cache = {'project_id': "test_project_id"}
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
self.assertEqual(
@ -277,7 +274,7 @@ class ProjectSetupActionTests(TestCase):
"""
setup_neutron_cache('RegionOne', 'test_project_id')
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
data = {
'setup_network': True,
@ -287,10 +284,10 @@ class ProjectSetupActionTests(TestCase):
action = NewProjectDefaultNetworkAction(
data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, False)
self.assertEqual(action.action.cache, {})
@ -309,7 +306,7 @@ class ProjectSetupActionTests(TestCase):
"""
setup_neutron_cache('RegionOne', 'test_project_id')
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
data = {
'setup_network': False,
@ -319,7 +316,7 @@ class ProjectSetupActionTests(TestCase):
action = NewProjectDefaultNetworkAction(
data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
# Now we add the project data as this is where the project
@ -334,7 +331,7 @@ class ProjectSetupActionTests(TestCase):
task.cache = {'project_id': "test_project_id"}
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
self.assertEqual(action.action.cache, {})
@ -354,7 +351,7 @@ class ProjectSetupActionTests(TestCase):
setup_neutron_cache('RegionOne', 'test_project_id')
global neutron_cache
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
data = {
'setup_network': True,
@ -364,7 +361,7 @@ class ProjectSetupActionTests(TestCase):
action = NewProjectDefaultNetworkAction(
data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
neutron_cache['RegionOne']['test_project_id']['routers'] = []
@ -382,7 +379,7 @@ class ProjectSetupActionTests(TestCase):
task.cache = {'project_id': "test_project_id"}
try:
action.post_approve()
action.approve()
self.fail("Shouldn't get here.")
except Exception:
pass
@ -402,7 +399,7 @@ class ProjectSetupActionTests(TestCase):
neutron_cache['RegionOne']['test_project_id']['routers'] = {}
action.post_approve()
action.approve()
self.assertEqual(
action.action.cache,
@ -433,16 +430,16 @@ class ProjectSetupActionTests(TestCase):
setup_mock_caches('RegionOne', 'test_project_id')
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
task.cache = {'project_id': "test_project_id"}
action = SetProjectQuotaAction({}, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
# check the quotas were updated
@ -502,7 +499,7 @@ class QuotaActionTests(TestCase):
# Test sending to only a single region
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
data = {
'project_id': 'test_project_id',
@ -513,10 +510,10 @@ class QuotaActionTests(TestCase):
action = UpdateProjectQuotasAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
# check the quotas were updated
@ -550,7 +547,7 @@ class QuotaActionTests(TestCase):
setup_mock_caches('RegionTwo', project.id)
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
data = {
'project_id': 'test_project_id',
@ -562,10 +559,10 @@ class QuotaActionTests(TestCase):
action = UpdateProjectQuotasAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
# check the quotas were updated
@ -609,7 +606,7 @@ class QuotaActionTests(TestCase):
setup_mock_caches('RegionTwo', project.id)
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
data = {
'project_id': 'test_project_id',
@ -620,10 +617,10 @@ class QuotaActionTests(TestCase):
action = UpdateProjectQuotasAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
# check the quotas were updated
@ -666,7 +663,7 @@ class QuotaActionTests(TestCase):
setup_mock_caches('RegionOne', project.id)
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
data = {
'project_id': 'test_project_id',
@ -677,10 +674,10 @@ class QuotaActionTests(TestCase):
action = UpdateProjectQuotasAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
# check the quotas were updated
@ -718,7 +715,7 @@ class QuotaActionTests(TestCase):
setup_mock_caches('RegionOne', project.id)
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
keystone_user={'roles': ['admin']})
data = {
'project_id': 'test_project_id',
@ -734,10 +731,10 @@ class QuotaActionTests(TestCase):
action = UpdateProjectQuotasAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, False)
action.post_approve()
action.approve()
self.assertEqual(action.valid, False)
# check the quotas were updated

View File

@ -39,7 +39,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(projects=[project])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': project.id,
@ -56,10 +55,10 @@ class UserActionTests(AdjutantTestCase):
action = NewUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {'password': '123456'}
@ -90,7 +89,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(projects=[project], users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': project.id,
@ -107,10 +105,10 @@ class UserActionTests(AdjutantTestCase):
action = NewUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {}
@ -136,7 +134,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(projects=[project], users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': project.id,
@ -153,10 +150,10 @@ class UserActionTests(AdjutantTestCase):
action = NewUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {'password': '123456'}
@ -198,7 +195,6 @@ class UserActionTests(AdjutantTestCase):
projects=[project], users=[user], role_assignments=[assignment])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': project.id,
@ -215,10 +211,10 @@ class UserActionTests(AdjutantTestCase):
action = NewUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
self.assertEqual(action.action.state, 'complete')
@ -239,7 +235,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache()
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id',
@ -256,10 +251,10 @@ class UserActionTests(AdjutantTestCase):
action = NewUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, False)
action.post_approve()
action.approve()
self.assertEqual(action.valid, False)
token_data = {}
@ -281,7 +276,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(projects=[project], users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': 'test_project_id',
@ -298,7 +292,7 @@ class UserActionTests(AdjutantTestCase):
action = NewUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, False)
def test_new_user_only_member(self):
@ -316,7 +310,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(projects=[project], users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['_member_'],
'project_id': project.id,
@ -333,7 +326,7 @@ class UserActionTests(AdjutantTestCase):
action = NewUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertFalse(action.valid)
def test_new_user_wrong_domain(self):
@ -358,7 +351,6 @@ class UserActionTests(AdjutantTestCase):
projects=[project], users=[user], role_assignments=[assignment])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_admin'],
'project_id': project.id,
@ -375,7 +367,7 @@ class UserActionTests(AdjutantTestCase):
action = NewUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertFalse(action.valid)
def test_reset_user_password(self):
@ -390,7 +382,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id',
@ -404,10 +395,10 @@ class UserActionTests(AdjutantTestCase):
action = ResetUserPasswordAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {'password': '123456'}
@ -432,7 +423,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id',
@ -446,10 +436,10 @@ class UserActionTests(AdjutantTestCase):
action = ResetUserPasswordAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {'password': '123456'}
@ -468,7 +458,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache()
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id',
@ -482,10 +471,10 @@ class UserActionTests(AdjutantTestCase):
action = ResetUserPasswordAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, False)
action.post_approve()
action.approve()
self.assertEqual(action.valid, False)
token_data = {}
@ -505,7 +494,6 @@ class UserActionTests(AdjutantTestCase):
projects=[project], users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': project.id,
@ -523,10 +511,10 @@ class UserActionTests(AdjutantTestCase):
action = EditUserRolesAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {}
@ -564,7 +552,6 @@ class UserActionTests(AdjutantTestCase):
projects=[project], users=[user], role_assignments=assignments)
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': project.id,
@ -582,11 +569,11 @@ class UserActionTests(AdjutantTestCase):
action = EditUserRolesAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
self.assertEqual(action.action.state, "complete")
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {}
@ -625,7 +612,6 @@ class UserActionTests(AdjutantTestCase):
projects=[project], users=[user], role_assignments=assignments)
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': project.id,
@ -643,10 +629,10 @@ class UserActionTests(AdjutantTestCase):
action = EditUserRolesAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {}
@ -678,7 +664,6 @@ class UserActionTests(AdjutantTestCase):
projects=[project], users=[user], role_assignments=[assignment])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': project.id,
@ -696,11 +681,11 @@ class UserActionTests(AdjutantTestCase):
action = EditUserRolesAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
self.assertEqual(action.action.state, "complete")
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {}
@ -739,7 +724,6 @@ class UserActionTests(AdjutantTestCase):
projects=[project], users=[user], role_assignments=assignments)
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': project.id,
@ -757,7 +741,7 @@ class UserActionTests(AdjutantTestCase):
action = EditUserRolesAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, False)
fake_client = fake_clients.FakeManager()
@ -785,7 +769,6 @@ class UserActionTests(AdjutantTestCase):
projects=[project], users=[user], role_assignments=[assignment])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': project.id,
@ -803,7 +786,7 @@ class UserActionTests(AdjutantTestCase):
action = EditUserRolesAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
# Change settings
@ -811,7 +794,7 @@ class UserActionTests(AdjutantTestCase):
'key_list': ['project_mod'],
'operation': "remove",
'value': 'heat_stack_owner'}):
action.post_approve()
action.approve()
self.assertEqual(action.valid, False)
token_data = {}
@ -819,7 +802,7 @@ class UserActionTests(AdjutantTestCase):
self.assertEqual(action.valid, False)
# After Settings Reset
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {}
@ -857,7 +840,6 @@ class UserActionTests(AdjutantTestCase):
fake_clients.identity_cache['roles'][new_role.id] = new_role
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': project.id,
@ -875,10 +857,10 @@ class UserActionTests(AdjutantTestCase):
action = EditUserRolesAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {}
@ -903,7 +885,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(projects=[project])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': project.id,
@ -921,10 +902,10 @@ class UserActionTests(AdjutantTestCase):
action = NewUserAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {'password': '123456'}
@ -956,7 +937,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': 'test_project_id',
@ -971,10 +951,10 @@ class UserActionTests(AdjutantTestCase):
action = ResetUserPasswordAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {'password': '123456'}
@ -1002,7 +982,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id',
@ -1017,10 +996,10 @@ class UserActionTests(AdjutantTestCase):
action = ResetUserPasswordAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {'password': '123456'}
@ -1043,7 +1022,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': 'test_project_id',
@ -1057,10 +1035,10 @@ class UserActionTests(AdjutantTestCase):
action = UpdateUserEmailAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
token_data = {'confirm': True}
@ -1084,7 +1062,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache()
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': 'test_project_id',
@ -1098,10 +1075,10 @@ class UserActionTests(AdjutantTestCase):
action = UpdateUserEmailAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, False)
action.post_approve()
action.approve()
self.assertEqual(action.valid, False)
token_data = {'confirm': True}
@ -1121,7 +1098,6 @@ class UserActionTests(AdjutantTestCase):
setup_identity_cache(users=[user])
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': 'test_project_id',
@ -1135,10 +1111,10 @@ class UserActionTests(AdjutantTestCase):
action = UpdateUserEmailAction(data, task=task, order=1)
action.pre_approve()
action.prepare()
self.assertEqual(action.valid, True)
action.post_approve()
action.approve()
self.assertEqual(action.valid, True)
action.submit({'confirm': True})

View File

@ -115,11 +115,11 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
])
self.action.save()
def _pre_approve(self):
def _prepare(self):
self._validate()
self.set_auto_approve()
def _post_approve(self):
def _approve(self):
self._validate()
def _submit(self, token_data):
@ -228,10 +228,11 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
])
self.action.save()
def _pre_approve(self):
def _prepare(self):
self._validate()
self.set_auto_approve()
def _post_approve(self):
def _approve(self):
self._validate()
def _submit(self, token_data):
@ -336,11 +337,11 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
])
self.action.save()
def _pre_approve(self):
def _prepare(self):
self._validate()
self.set_auto_approve()
def _post_approve(self):
def _approve(self):
self._validate()
def _submit(self, token_data):
@ -425,11 +426,11 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
self.add_note("No user with same username")
return True
def _pre_approve(self):
def _prepare(self):
self._validate()
self.set_auto_approve(True)
def _post_approve(self):
def _approve(self):
self._validate()
self.action.need_token = True
self.set_token_fields(["confirm"])

View File

@ -0,0 +1,58 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from logging import getLogger
from django.http import Http404
from django.utils import timezone
from rest_framework.response import Response
from adjutant import exceptions
from adjutant.api.v1.utils import create_notification
LOG = getLogger('adjutant')
def exception_handler(exc, context):
"""Returns the response that should be used for any given exception.
"""
now = timezone.now()
if isinstance(exc, Http404):
exc = exceptions.NotFound()
elif isinstance(exc, exceptions.BaseServiceException):
LOG.exception("(%s) - Internal service error." % now)
exc = exceptions.ServiceUnavailable()
if isinstance(exc, exceptions.BaseAPIException):
if isinstance(exc.message, (list, dict)):
data = {'errors': exc.message}
else:
data = {'errors': [exc.message]}
note_data = data
if isinstance(exc, exceptions.TaskActionsFailed):
if exc.internal_message:
if isinstance(exc.internal_message, (list, dict)):
note_data = {'errors': exc.internal_message}
else:
note_data = {'errors': [exc.internal_message]}
create_notification(exc.task, note_data, error=True)
LOG.info("(%s) - %s" % (now, exc))
return Response(data, status=exc.status_code)
LOG.exception("(%s) - Internal service error." % now)
return None

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.21 on 2019-06-10 02:09
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('api', '0004_auto_20160929_0317'),
]
operations = [
migrations.SeparateDatabaseAndState(
database_operations=[
migrations.AlterModelTable(
name='task',
table='tasks_task',
),
],
),
]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.21 on 2019-06-10 02:09
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tasks', '0001_initial'),
('actions', '0003_auto_20190610_0205'),
]
operations = [
migrations.AlterField(
model_name='token',
name='task',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.Task'),
),
]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.21 on 2019-06-10 02:09
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tasks', '0001_initial'),
('actions', '0003_auto_20190610_0205'),
]
operations = [
migrations.AlterField(
model_name='notification',
name='task',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.Task'),
),
]

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.21 on 2019-06-10 02:09
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('api', '0005_auto_20190610_0209'),
('tasks', '0001_initial'),
('actions', '0004_auto_20190610_0209'),
('api', '0006_auto_20190610_0209'),
('api', '0007_auto_20190610_0209'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.DeleteModel(
name='Task',
),
],
),
]

View File

@ -17,102 +17,13 @@ from uuid import uuid4
from django.utils import timezone
from jsonfield import JSONField
from adjutant.tasks.models import Task
def hex_uuid():
return uuid4().hex
class Task(models.Model):
"""
Wrapper object for the request and related actions.
Stores the state of the Task and a log for the
action.
"""
uuid = models.CharField(max_length=32, default=hex_uuid,
primary_key=True)
hash_key = models.CharField(max_length=64, db_index=True)
# who is this:
ip_address = models.GenericIPAddressField()
keystone_user = JSONField(default={})
project_id = models.CharField(max_length=64, db_index=True, null=True)
# keystone_user for the approver:
approved_by = JSONField(default={})
# type of the task, for easy grouping
task_type = models.CharField(max_length=100, db_index=True)
# Effectively a log of what the actions are doing.
action_notes = JSONField(default={})
cancelled = models.BooleanField(default=False, db_index=True)
approved = models.BooleanField(default=False, db_index=True)
completed = models.BooleanField(default=False, db_index=True)
created_on = models.DateTimeField(default=timezone.now)
approved_on = models.DateTimeField(null=True)
completed_on = models.DateTimeField(null=True)
def __init__(self, *args, **kwargs):
super(Task, self).__init__(*args, **kwargs)
# in memory dict to be used for passing data between actions:
self.cache = {}
@property
def actions(self):
return self.action_set.order_by('order')
@property
def tokens(self):
return self.token_set.all()
@property
def notifications(self):
return self.notification_set.all()
def _to_dict(self):
actions = []
for action in self.actions:
actions.append({
"action_name": action.action_name,
"data": action.action_data,
"valid": action.valid
})
return {
"uuid": self.uuid,
"ip_address": self.ip_address,
"keystone_user": self.keystone_user,
"approved_by": self.approved_by,
"project_id": self.project_id,
"actions": actions,
"task_type": self.task_type,
"action_notes": self.action_notes,
"cancelled": self.cancelled,
"approved": self.approved,
"completed": self.completed,
"created_on": self.created_on,
"approved_on": self.approved_on,
"completed_on": self.completed_on,
}
def to_dict(self):
"""
Slightly safer variant of the above for non-admin.
"""
task_dict = self._to_dict()
task_dict.pop("ip_address")
return task_dict
def add_action_note(self, action, note):
if action in self.action_notes:
self.action_notes[action].append(note)
else:
self.action_notes[action] = [note]
self.save()
class Token(models.Model):
"""
UUID token object bound to a task.

22
adjutant/api/v1/base.py Normal file
View File

@ -0,0 +1,22 @@
# 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.
from adjutant.api.v1.views import APIViewWithLogger
# 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

View File

@ -16,38 +16,44 @@ from django.conf import settings
from adjutant.api.v1 import tasks
from adjutant.api.v1 import openstack
from adjutant.api.v1.base import BaseDelegateAPI
from adjutant import exceptions
def register_taskview_class(url, taskview_class):
def register_delegate_api_class(url, API_class):
if not issubclass(API_class, BaseDelegateAPI):
raise exceptions.InvalidAPIClass(
"'%s' is not a built off the BaseDelegateAPI class."
% API_class.__name__
)
data = {}
data[taskview_class.__name__] = {
'class': taskview_class,
data[API_class.__name__] = {
'class': API_class,
'url': url}
settings.TASKVIEW_CLASSES.update(data)
settings.DELEGATE_API_CLASSES.update(data)
register_taskview_class(r'^actions/CreateProject/?$', tasks.CreateProject)
register_taskview_class(r'^actions/InviteUser/?$', tasks.InviteUser)
register_taskview_class(r'^actions/ResetPassword/?$', tasks.ResetPassword)
register_taskview_class(r'^actions/EditUser/?$', tasks.EditUser)
register_taskview_class(r'^actions/UpdateEmail/?$', tasks.UpdateEmail)
register_delegate_api_class(
r'^actions/CreateProjectAndUser/?$', tasks.CreateProjectAndUser)
register_delegate_api_class(r'^actions/InviteUser/?$', tasks.InviteUser)
register_delegate_api_class(r'^actions/ResetPassword/?$', tasks.ResetPassword)
register_delegate_api_class(r'^actions/EditUser/?$', tasks.EditUser)
register_delegate_api_class(r'^actions/UpdateEmail/?$', tasks.UpdateEmail)
register_taskview_class(
register_delegate_api_class(
r'^openstack/users/?$', openstack.UserList)
register_taskview_class(
register_delegate_api_class(
r'^openstack/users/(?P<user_id>\w+)/?$', openstack.UserDetail)
register_taskview_class(
register_delegate_api_class(
r'^openstack/users/(?P<user_id>\w+)/roles/?$', openstack.UserRoles)
register_taskview_class(
register_delegate_api_class(
r'^openstack/roles/?$', openstack.RoleList)
register_taskview_class(
register_delegate_api_class(
r'^openstack/users/password-reset/?$', openstack.UserResetPassword)
register_taskview_class(
r'^openstack/users/password-set/?$', openstack.UserSetPassword)
register_taskview_class(
register_delegate_api_class(
r'^openstack/users/email-update/?$', openstack.UserUpdateEmail)
register_taskview_class(
register_delegate_api_class(
r'^openstack/sign-up/?$', openstack.SignUp)
register_taskview_class(
register_delegate_api_class(
r'^openstack/quotas/?$', openstack.UpdateProjectQuotas)

View File

@ -22,7 +22,7 @@ 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.utils import add_task_id_for_roles, create_notification
from adjutant.api.v1.base import BaseDelegateAPI
from adjutant.common.quota import QuotaManager
@ -32,7 +32,7 @@ class UserList(tasks.InviteUser):
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', settings.DEFAULT_TASK_SETTINGS)
'edit_user_roles', settings.DEFAULT_TASK_SETTINGS)
role_blacklist = class_conf.get('role_blacklist', [])
user_list = []
id_manager = user_store.IdentityManager()
@ -63,7 +63,7 @@ class UserList(tasks.InviteUser):
continue
email = getattr(user, 'email', '')
enabled = getattr(user, 'enabled')
enabled = user.enabled
user_status = 'Active' if enabled else 'Account Disabled'
active_emails.add(email)
user_list.append({
@ -89,7 +89,7 @@ class UserList(tasks.InviteUser):
continue
email = getattr(user, 'email', '')
enabled = getattr(user, 'enabled')
enabled = user.enabled
user_status = 'Active' if enabled else 'Account Disabled'
user_list.append({'id': user.id,
'name': user.name,
@ -104,7 +104,7 @@ class UserList(tasks.InviteUser):
# Get my active tasks for this project:
project_tasks = models.Task.objects.filter(
project_id=project_id,
task_type="invite_user",
task_type="invite_user_to_project",
completed=0,
cancelled=0)
@ -153,8 +153,8 @@ class UserList(tasks.InviteUser):
return Response({'users': user_list})
class UserDetail(tasks.TaskView):
task_type = 'edit_user'
class UserDetail(BaseDelegateAPI):
task_type = 'edit_user_roles'
@utils.mod_or_admin
def get(self, request, user_id):
@ -209,22 +209,19 @@ class UserDetail(tasks.TaskView):
status=501)
project_tasks = models.Task.objects.filter(
project_id=project_id,
task_type="invite_user",
task_type="invite_user_to_project",
completed=0,
cancelled=0)
for task in project_tasks:
if task.uuid == user_id:
task.add_action_note(self.__class__.__name__, 'Cancelled.')
task.cancelled = True
task.save()
self.task_manager.cancel(task)
return Response('Cancelled pending invite task!', status=200)
return Response('Not found.', status=404)
class UserRoles(tasks.TaskView):
class UserRoles(BaseDelegateAPI):
default_actions = ['EditUserRolesAction', ]
task_type = 'edit_roles'
task_type = 'edit_user_roles'
@utils.mod_or_admin
def get(self, request, user_id):
@ -279,23 +276,13 @@ class UserRoles(tasks.TaskView):
self.logger.info("(%s) - New EditUser %s request." % (
timezone.now(), request.method))
processed, status = self.process_actions(request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with registration." %
timezone.now())
return Response({'errors': errors}, status=status)
self.task_manager.create_from_request(self.task_type, request)
response_dict = {'notes': processed.get('notes')}
add_task_id_for_roles(request, processed, response_dict, ['admin'])
return Response(response_dict, status=status)
return Response({'notes': ['task created']}, status=202)
class RoleList(tasks.TaskView):
task_type = 'edit_roles'
class RoleList(BaseDelegateAPI):
@utils.mod_or_admin
def get(self, request):
@ -323,32 +310,7 @@ class UserResetPassword(tasks.ResetPassword):
---
"""
def get(self, request):
"""
The ResetPassword endpoint does not support GET.
This returns a 404.
"""
return Response(status=404)
class UserSetPassword(tasks.ResetPassword):
"""
The openstack endpoint to force a password reset.
---
"""
task_type = "force_password"
def get(self, request):
"""
The ForcePassword endpoint does not support GET.
This returns a 404.
"""
return Response(status=404)
@utils.admin
def post(self, request, format=None):
return super(UserSetPassword, self).post(request)
pass
class UserUpdateEmail(tasks.UpdateEmail):
@ -357,40 +319,24 @@ class UserUpdateEmail(tasks.UpdateEmail):
---
"""
def get(self, request):
"""
The EmailUpdate endpoint does not support GET.
This returns a 404.
"""
return Response(status=404)
pass
class SignUp(tasks.CreateProject):
class SignUp(tasks.CreateProjectAndUser):
"""
The openstack endpoint for signups.
"""
task_type = "signup"
def get(self, request):
"""
The SignUp endpoint does not support GET.
This returns a 404.
"""
return Response(status=404)
def post(self, request, format=None):
return super(SignUp, self).post(request)
pass
class UpdateProjectQuotas(tasks.TaskView):
class UpdateProjectQuotas(BaseDelegateAPI):
"""
The OpenStack endpoint to update the quota of a project in
one or more regions
"""
task_type = "update_quota"
default_actions = ["UpdateProjectQuotasAction", ]
_number_of_returned_tasks = 5
@ -490,36 +436,6 @@ class UpdateProjectQuotas(tasks.TaskView):
self.logger.info("(%s) - New UpdateProjectQuotas request."
% timezone.now())
processed, status = self.process_actions(request)
self.task_manager.create_from_request(self.task_type, request)
# check the status
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with task." %
timezone.now())
return Response({'errors': errors}, status=status)
if processed.get('auto_approved', False):
response_dict = {'notes': processed['notes']}
return Response(response_dict, status=status)
task = processed['task']
action_models = task.actions
valid = all([act.valid for act in action_models])
if not valid:
return Response({'errors': ['Actions invalid. You may have usage '
'above the new quota level.']}, 400)
# Action needs to be manually approved
notes = {
'notes':
['New task for UpdateProjectQuotas.']
}
create_notification(processed['task'], notes)
self.logger.info("(%s) - Task processed. Awaiting Aprroval"
% timezone.now())
response_dict = {'notes': ['Task processed. Awaiting Aprroval.']}
return Response(response_dict, status=202)
return Response({'notes': ['task created']}, status=202)

View File

@ -12,298 +12,22 @@
# License for the specific language governing permissions and limitations
# under the License.
from rest_framework.response import Response
from adjutant.common.user_store import IdentityManager
from adjutant.api.models import Task
from django.utils import timezone
from adjutant.api import utils
from adjutant.api.v1.views import APIViewWithLogger
from adjutant.api.v1.utils import (
send_stage_email, create_notification, create_token, create_task_hash,
add_task_id_for_roles)
from adjutant.exceptions import SerializerMissingException
from django.conf import settings
from django.utils import timezone
from rest_framework.response import Response
from adjutant import exceptions
from adjutant.api import utils
from adjutant.api.v1.base import BaseDelegateAPI
class TaskView(APIViewWithLogger):
"""
Base class for api calls that start a Task.
'default_actions' is a required hardcoded field.
# NOTE(adriant): We should deprecate these Views properly and switch tests
# to work against the openstack ones.
The default_actions are considered the primary actions and
will always run first (in the given order). Additional actions
are defined in the settings file and will run in the order supplied,
but after the default_actions.
class CreateProjectAndUser(BaseDelegateAPI):
Default actions can be overridden in the settings file as well if
needed.
"""
default_actions = []
def get(self, request):
"""
The get method will return a json listing the actions this
view will run, and the data fields that those actions require.
"""
class_conf = settings.TASK_SETTINGS.get(
self.task_type, settings.DEFAULT_TASK_SETTINGS)
actions = (
class_conf.get('default_actions', [])
or self.default_actions[:])
actions += class_conf.get('additional_actions', [])
required_fields = []
for action in actions:
action_class, action_serializer = settings.ACTION_CLASSES[action]
for field in action_class.required:
if field not in required_fields:
required_fields.append(field)
return Response({'actions': actions,
'required_fields': required_fields})
def _instantiate_action_serializers(self, request, class_conf):
action_serializer_list = []
action_names = (
class_conf.get('default_actions', [])
or self.default_actions[:])
action_names += class_conf.get('additional_actions', [])
# instantiate all action serializers and check validity
valid = True
for action_name in action_names:
action_class, serializer_class = \
settings.ACTION_CLASSES[action_name]
# instantiate serializer class
if not serializer_class:
raise SerializerMissingException(
"No serializer defined for action %s" % action_name)
serializer = serializer_class(data=request.data)
action_serializer_list.append({
'name': action_name,
'action': action_class,
'serializer': serializer})
if serializer and not serializer.is_valid():
valid = False
if not valid:
errors = {}
for action in action_serializer_list:
if action['serializer']:
errors.update(action['serializer'].errors)
return {'errors': errors}, 400
return action_serializer_list
def _handle_duplicates(self, class_conf, hash_key):
duplicate_tasks = Task.objects.filter(
hash_key=hash_key,
completed=0,
cancelled=0)
if not duplicate_tasks:
return False
duplicate_policy = class_conf.get("duplicate_policy", "")
if duplicate_policy == "cancel":
self.logger.info(
"(%s) - Task is a duplicate - Cancelling old tasks." %
timezone.now())
for task in duplicate_tasks:
task.cancelled = True
task.save()
return False
self.logger.info(
"(%s) - Task is a duplicate - Ignoring new task." %
timezone.now())
return (
{'errors': ['Task is a duplicate of an existing task']},
409)
def process_actions(self, request):
"""
Will ensure the request data contains the required data
based on the action serializer, and if present will create
a Task and the linked actions, attaching notes
based on running of the pre_approve validation
function on all the actions.
If during the pre_approve step at least one of the actions
sets auto_approve to True, and none of them set it to False
the approval steps will also be run.
"""
class_conf = settings.TASK_SETTINGS.get(
self.task_type, settings.DEFAULT_TASK_SETTINGS)
# Action serializers
action_serializer_list = self._instantiate_action_serializers(
request, class_conf)
if isinstance(action_serializer_list, tuple):
return action_serializer_list
hash_key = create_task_hash(self.task_type, action_serializer_list)
# Handle duplicates
duplicate_error = self._handle_duplicates(class_conf, hash_key)
if duplicate_error:
return duplicate_error
# Instantiate Task
ip_address = request.META['REMOTE_ADDR']
keystone_user = request.keystone_user
task = Task.objects.create(
ip_address=ip_address,
keystone_user=keystone_user,
project_id=keystone_user.get('project_id'),
task_type=self.task_type,
hash_key=hash_key)
task.save()
# Instantiate actions with serializers
action_instances = []
for i, action in enumerate(action_serializer_list):
data = action['serializer'].validated_data
# construct the action class
action_instances.append(action['action'](
data=data,
task=task,
order=i
))
# We run pre_approve on the actions once we've setup all of them.
for action_instance in action_instances:
try:
action_instance.pre_approve()
except Exception as e:
return self._handle_task_error(
e, task, error_text='while setting up task')
# send initial confirmation email:
email_conf = class_conf.get('emails', {}).get('initial', None)
send_stage_email(task, email_conf)
approve_list = [act.auto_approve for act in action_instances]
# TODO(amelia): It would be nice to explicitly test this, however
# currently we don't have the right combinations of
# actions to allow for it.
if False in approve_list:
can_auto_approve = False
elif True in approve_list:
can_auto_approve = True
else:
can_auto_approve = False
if can_auto_approve:
task_name = self.__class__.__name__
self.logger.info("(%s) - AutoApproving %s request."
% (timezone.now(), task_name))
approval_data, status = self.approve(request, task)
# Additional information that would be otherwise expected
approval_data['task'] = task
approval_data['auto_approved'] = True
return approval_data, status
return {'task': task}, 200
def _create_token(self, task):
token = create_token(task)
try:
class_conf = settings.TASK_SETTINGS.get(
self.task_type, settings.DEFAULT_TASK_SETTINGS)
# will throw a key error if the token template has not
# been specified
email_conf = class_conf['emails']['token']
send_stage_email(task, email_conf, token)
return {'notes': ['created token']}, 200
except KeyError as e:
return self._handle_task_error(
e, task, error_text='while sending token')
def approve(self, request, task):
"""
Approves the task and runs the post_approve steps.
Will create a token if required, otherwise will run the
submit steps.
"""
# cannot approve an invalid task
action_models = task.actions
actions = [act.get_action() for act in action_models]
valid = all([act.valid for act in actions])
if not valid:
return {'errors': ['actions invalid']}, 400
# TODO(amelia): get action invalidation reasons
# We approve the task before running actions,
# that way if something goes wrong we know if it was approved,
# when it was approved, and who approved it.
task.approved = True
task.approved_on = timezone.now()
task.approved_by = request.keystone_user
task.save()
need_token = False
# post_approve all actions
for action in actions:
try:
action.post_approve()
except Exception as e:
return self._handle_task_error(
e, task, error_text='while approving task')
valid = all([act.valid for act in actions])
if not valid:
return {'errors': ['actions invalid']}, 400
need_token = any([act.need_token for act in actions])
if need_token:
return self._create_token(task)
# submit all actions
for action in actions:
try:
action.submit({})
except Exception as e:
self._handle_task_error(
e, task, error_text='while submitting task')
task.completed = True
task.completed_on = timezone.now()
task.save()
# Sending confirmation email:
class_conf = settings.TASK_SETTINGS.get(
self.task_type, settings.DEFAULT_TASK_SETTINGS)
email_conf = class_conf.get(
'emails', {}).get('completed', None)
send_stage_email(task, email_conf)
return {'notes': ["Task completed successfully."]}, 200
# NOTE(adriant): We should deprecate these TaskViews properly and switch tests
# to work against the openstack ones. One option is making these abstract
# classes, so we retain the code here, but make them useless without extension.
class CreateProject(TaskView):
task_type = "create_project"
default_actions = ["NewProjectWithUserAction", ]
task_type = "create_project_and_user"
def post(self, request, format=None):
"""
@ -328,33 +52,14 @@ class CreateProject(TaskView):
# parent_id for new project, if null defaults to domain:
request.data['parent_id'] = class_conf.get('default_parent_id')
processed, status = self.process_actions(request)
self.task_manager.create_from_request(self.task_type, request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with task." %
timezone.now())
return Response({'errors': errors}, status=status)
notes = {
'notes':
['New task for CreateProject.']
}
create_notification(processed['task'], notes)
self.logger.info("(%s) - Task created." % timezone.now())
response_dict = {'notes': ['task created']}
add_task_id_for_roles(request, processed, response_dict, ['admin'])
return Response(response_dict, status=status)
return Response({'notes': ['task created']}, status=202)
class InviteUser(TaskView):
class InviteUser(BaseDelegateAPI):
task_type = "invite_user"
default_actions = ['NewUserAction', ]
task_type = "invite_user_to_project"
@utils.mod_or_admin
def get(self, request):
@ -382,27 +87,14 @@ class InviteUser(TaskView):
request.data['domain_id'] = \
request.keystone_user['project_domain_id']
processed, status = self.process_actions(request)
self.task_manager.create_from_request(self.task_type, request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with task." %
timezone.now())
return Response({'errors': errors}, status=status)
response_dict = {'notes': processed['notes']}
add_task_id_for_roles(request, processed, response_dict, ['admin'])
return Response(response_dict, status=status)
return Response({'notes': ['task created']}, status=202)
class ResetPassword(TaskView):
class ResetPassword(BaseDelegateAPI):
task_type = "reset_password"
default_actions = ['ResetUserPasswordAction', ]
task_type = "reset_user_password"
@utils.minimal_duration(min_time=3)
def post(self, request, format=None):
@ -429,77 +121,22 @@ class ResetPassword(TaskView):
"""
self.logger.info("(%s) - New ResetUser request." % timezone.now())
processed, status = self.process_actions(request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with task." %
timezone.now())
return Response({'errors': errors}, status=status)
try:
self.task_manager.create_from_request(self.task_type, request)
except exceptions.BaseTaskException as e:
self.logger.info(
"(%s) - ResetPassword raised error: %s" % (timezone.now(), e))
task = processed['task']
self.logger.info("(%s) - AutoApproving Resetuser request."
% timezone.now())
# NOTE(amelia): Not using auto approve due to security implications
# as it will return all errors including whether the user exists
self.approve(request, task)
response_dict = {'notes': [
"If user with email exists, reset token will be issued."]}
add_task_id_for_roles(request, processed, response_dict, ['admin'])
return Response(response_dict, status=200)
return Response(response_dict, status=202)
class EditUser(TaskView):
class EditUser(BaseDelegateAPI):
task_type = "edit_user"
default_actions = ['EditUserRolesAction', ]
@utils.mod_or_admin
def get(self, request):
class_conf = settings.TASK_SETTINGS.get(
self.task_type, settings.DEFAULT_TASK_SETTINGS)
action_names = (
class_conf.get('default_actions', [])
or self.default_actions[:])
action_names += class_conf.get('additional_actions', [])
role_blacklist = class_conf.get('role_blacklist', [])
required_fields = set()
for action_name in action_names:
action_class, action_serializer = \
settings.ACTION_CLASSES[action_name]
required_fields |= action_class.required
user_list = []
id_manager = IdentityManager()
project_id = request.keystone_user['project_id']
project = id_manager.get_project(project_id)
# todo: move to interface class
for user in id_manager.list_users(project):
skip = False
roles = []
for role in user.roles:
if role.name in role_blacklist:
skip = True
continue
roles.append(role.name)
if skip:
continue
user_list.append({"username": user.username,
"email": user.username,
"roles": roles})
return Response({'actions': action_names,
'required_fields': list(required_fields),
'users': user_list})
task_type = "edit_user_roles"
@utils.mod_or_admin
def post(self, request, format=None):
@ -508,27 +145,17 @@ class EditUser(TaskView):
request to come from a project_admin.
As such this Task is considered pre-approved.
Runs process_actions, then does the approve step and
post_approve validation, and creates a Token if valid.
approve validation, and creates a Token if valid.
"""
self.logger.info("(%s) - New EditUser request." % timezone.now())
processed, status = self.process_actions(request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with task." %
timezone.now())
return Response({'errors': errors}, status=status)
self.task_manager.create_from_request(self.task_type, request)
response_dict = {'notes': processed.get('notes')}
add_task_id_for_roles(request, processed, response_dict, ['admin'])
return Response(response_dict, status=status)
return Response({'notes': ['task created']}, status=202)
class UpdateEmail(TaskView):
task_type = "update_email"
default_actions = ["UpdateUserEmailAction", ]
class UpdateEmail(BaseDelegateAPI):
task_type = "update_user_email"
@utils.authenticated
def post(self, request, format=None):
@ -539,13 +166,6 @@ class UpdateEmail(TaskView):
request.data['user_id'] = request.keystone_user['user_id']
processed, status = self.process_actions(request)
self.task_manager.create_from_request(self.task_type, request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with task." %
timezone.now())
return Response({'errors': errors}, status=status)
response_dict = {'notes': processed['notes']}
return Response(response_dict, status=status)
return Response({'notes': ['task created']}, status=202)

View File

@ -1,6 +0,0 @@
This email is to confirm that your Openstack account password has now been set up.
If you did not do this yourself, please get in touch with your systems administrator to report suspicious activity and secure your account.
Kind regards,
The Openstack team

View File

@ -1,15 +0,0 @@
Hello,
Thank you for joining Openstack!
Please follow this link to define your password to access Openstack:
{{ tokenurl }}{{ token }}
This link expires automatically after 24 hours. If expired, you can simply go to the dashboard and request a password reset.
Once this is done you will have access to the dashboard and APIs.
You can find examples and documentation on using Openstack at http://docs.openstack.org/
Kind regards,
The Openstack team

View File

@ -69,10 +69,10 @@ class AdminAPITests(APITestCase):
"""
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -118,10 +118,14 @@ class AdminAPITests(APITestCase):
'authenticated': True
}
url = "/v1/tasks/e8b3f57f5da64bf3a6bf4f9bbd3a40b5"
response = self.client.post(url, format='json', headers=headers)
response = self.client.post(
url, {'approved': True}, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
response.json(), {'errors': ['No task with this id.']})
response.json(),
{'errors': [
"Task not found with uuid of: "
"'e8b3f57f5da64bf3a6bf4f9bbd3a40b5'"]})
def test_token_expired_post(self):
"""
@ -136,7 +140,7 @@ class AdminAPITests(APITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
@ -166,7 +170,7 @@ class AdminAPITests(APITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
@ -195,7 +199,7 @@ class AdminAPITests(APITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
@ -208,7 +212,7 @@ class AdminAPITests(APITestCase):
response.json(),
{u'actions': [u'ResetUserPasswordAction'],
u'required_fields': [u'password'],
u'task_type': 'reset_password'})
u'task_type': 'reset_user_password'})
self.assertEqual(1, Token.objects.count())
def test_token_list_get(self):
@ -227,11 +231,11 @@ class AdminAPITests(APITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
data = {'email': "test2@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -259,10 +263,10 @@ class AdminAPITests(APITestCase):
"""
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -291,10 +295,10 @@ class AdminAPITests(APITestCase):
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -314,11 +318,11 @@ class AdminAPITests(APITestCase):
self.assertEqual(response.json()['error_notifications'], [])
# Create a second task and ensure it is the new last_created_task
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project_2",
'email': "test_2@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
url = "/v1/status/"
response = self.client.get(url, headers=headers)
@ -354,10 +358,10 @@ class AdminAPITests(APITestCase):
setup_identity_cache(projects=[project])
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -388,7 +392,7 @@ class AdminAPITests(APITestCase):
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json(),
{'notes': ['created token']})
@ -399,10 +403,10 @@ class AdminAPITests(APITestCase):
"""
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
new_task = Task.objects.all()[0]
@ -424,7 +428,7 @@ class AdminAPITests(APITestCase):
new_task.uuid)
self.assertEqual(
response.json()['notes'],
{u'notes': [u'New task for CreateProject.']})
{'notes': ["'create_project_and_user' task needs approval."]})
self.assertEqual(
response.json()['error'], False)
@ -451,7 +455,7 @@ class AdminAPITests(APITestCase):
{"errors": ["No notification with this id."]})
@modify_dict_settings(TASK_SETTINGS={
'key_list': ['create_project', 'notifications'],
'key_list': ['create_project_and_user', 'notifications'],
'operation': 'delete',
})
def test_notification_acknowledge(self):
@ -460,10 +464,10 @@ class AdminAPITests(APITestCase):
"""
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
new_task = Task.objects.all()[0]
@ -524,7 +528,7 @@ class AdminAPITests(APITestCase):
['No notification with this id.']})
@modify_dict_settings(TASK_SETTINGS={
'key_list': ['create_project', 'notifications'],
'key_list': ['create_project_and_user', 'notifications'],
'operation': 'delete',
})
def test_notification_re_acknowledge(self):
@ -533,10 +537,10 @@ class AdminAPITests(APITestCase):
"""
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -561,7 +565,7 @@ class AdminAPITests(APITestCase):
{'notes': ['Notification already acknowledged.']})
@modify_dict_settings(TASK_SETTINGS={
'key_list': ['create_project', 'notifications'],
'key_list': ['create_project_and_user', 'notifications'],
'operation': 'delete',
})
def test_notification_acknowledge_no_data(self):
@ -570,10 +574,10 @@ class AdminAPITests(APITestCase):
"""
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -598,13 +602,13 @@ class AdminAPITests(APITestCase):
"""
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
data = {'project_name': "test_project2", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -681,7 +685,7 @@ class AdminAPITests(APITestCase):
}
}
}, TASK_SETTINGS={
'key_list': ['create_project', 'emails'],
'key_list': ['create_project_and_user', 'emails'],
'operation': 'override',
'value': {
'initial': None,
@ -695,10 +699,10 @@ class AdminAPITests(APITestCase):
"""
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
new_task = Task.objects.all()[0]
@ -719,8 +723,11 @@ class AdminAPITests(APITestCase):
new_task.uuid)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'create_project notification')
self.assertTrue("New task for CreateProject" in mail.outbox[0].body)
self.assertEqual(
mail.outbox[0].subject, 'create_project_and_user notification')
self.assertTrue(
"'create_project_and_user' task needs approval."
in mail.outbox[0].body)
def test_token_expired_delete(self):
"""
@ -739,15 +746,14 @@ class AdminAPITests(APITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
url = "/v1/actions/ResetPassword"
data = {'email': "test2@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
@ -788,7 +794,7 @@ class AdminAPITests(APITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
@ -838,8 +844,8 @@ class AdminAPITests(APITestCase):
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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
task = Task.objects.all()[0]
new_token = Token.objects.all()[0]
@ -878,7 +884,7 @@ class AdminAPITests(APITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
@ -917,7 +923,7 @@ class AdminAPITests(APITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
@ -950,10 +956,10 @@ class AdminAPITests(APITestCase):
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'email': "test@example.com", "project_name": "test_project"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'], [u'task created'])
@ -982,10 +988,10 @@ class AdminAPITests(APITestCase):
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -1016,10 +1022,10 @@ class AdminAPITests(APITestCase):
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -1033,17 +1039,19 @@ class AdminAPITests(APITestCase):
url = "/v1/tasks/" + new_task.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
new_token = Token.objects.all()[0]
response = self.client.delete(url, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
new_token = Token.objects.all()[0]
self.assertEqual(0, Token.objects.count())
url = "/v1/tokens/" + new_token.token
data = {'password': 'testpassword'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_reapprove_task_delete_tokens(self):
"""
@ -1052,10 +1060,10 @@ class AdminAPITests(APITestCase):
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -1069,7 +1077,7 @@ class AdminAPITests(APITestCase):
url = "/v1/tasks/" + new_task.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(len(Token.objects.all()), 1)
new_token = Token.objects.all()[0]
@ -1081,7 +1089,7 @@ class AdminAPITests(APITestCase):
url = "/v1/tasks/" + new_task.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Old token no longer found
url = "/v1/tokens/" + new_token.token
@ -1097,10 +1105,10 @@ class AdminAPITests(APITestCase):
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -1115,7 +1123,7 @@ class AdminAPITests(APITestCase):
url = "/v1/tasks/" + new_task.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
new_task = Task.objects.all()[0]
self.assertEqual(new_task.approved, True)
@ -1145,8 +1153,8 @@ class AdminAPITests(APITestCase):
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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
new_task = Task.objects.all()[0]
url = "/v1/tasks/" + new_task.uuid
@ -1184,8 +1192,8 @@ class AdminAPITests(APITestCase):
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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
new_task = Task.objects.all()[0]
url = "/v1/tasks/" + new_task.uuid
@ -1214,15 +1222,15 @@ class AdminAPITests(APITestCase):
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_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
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_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
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_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -1258,15 +1266,15 @@ class AdminAPITests(APITestCase):
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_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
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_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
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_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -1306,16 +1314,16 @@ class AdminAPITests(APITestCase):
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_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
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_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project2", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -1327,7 +1335,7 @@ class AdminAPITests(APITestCase):
}
params = {
"filters": json.dumps({
"task_type": {"exact": "create_project"}
"task_type": {"exact": "create_project_and_user"}
})
}
@ -1340,7 +1348,7 @@ class AdminAPITests(APITestCase):
params = {
"filters": json.dumps({
"task_type": {"exact": "invite_user"}
"task_type": {"exact": "invite_user_to_project"}
})
}
response = self.client.get(
@ -1399,7 +1407,7 @@ class AdminAPITests(APITestCase):
params = {
"filters": json.dumps({
"project_id": {"exact": "test_project_id"},
"task_type": {"exact": "invite_user"}
"task_type": {"exact": "invite_user_to_project"}
})
}
url = "/v1/tasks"
@ -1478,7 +1486,7 @@ class AdminAPITests(APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@modify_dict_settings(TASK_SETTINGS={
'key_list': ['reset_password', 'action_settings',
'key_list': ['reset_user_password', 'action_settings',
'ResetUserPasswordAction', 'blacklisted_roles'],
'operation': 'append',
'value': ['admin']
@ -1506,7 +1514,7 @@ class AdminAPITests(APITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
@ -1515,18 +1523,18 @@ class AdminAPITests(APITestCase):
@mock.patch('adjutant.common.tests.fake_clients.FakeManager.find_project')
def test_apiview_error_handler(self, mocked_find):
"""
Ensure the _handle_task_error function works as expected for APIViews.
Ensure the handle_task_error function works as expected for APIViews.
"""
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(
response.status_code, status.HTTP_200_OK)
response.status_code, status.HTTP_202_ACCEPTED)
mocked_find.side_effect = KeyError("Forced key error.")
mocked_find.side_effect = KeyError("Forced key error for testing.")
new_task = Task.objects.all()[0]
url = "/v1/tasks/" + new_task.uuid
@ -1544,12 +1552,11 @@ class AdminAPITests(APITestCase):
}
response = self.client.put(url, data, format='json', headers=headers)
self.assertEqual(
response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
self.assertEqual(
response.json()['errors'],
["Error: Something went wrong on the server. "
"It will be looked into shortly."])
['Service temporarily unavailable, try again later.'])
new_task = Task.objects.all()[0]
new_notification = Notification.objects.all()[1]
@ -1558,6 +1565,6 @@ class AdminAPITests(APITestCase):
self.assertEqual(
new_notification.notes,
{'errors': [
"Error: KeyError('Forced key error.') while updating task. "
"See task itself for details."]})
"Error: KeyError('Forced key error for testing.') while "
"setting up task. See task itself for details."]})
self.assertEqual(new_notification.task, new_task)

View File

@ -38,10 +38,10 @@ from datetime import timedelta
FakeManager)
class OpenstackAPITests(AdjutantAPITestCase):
"""
TaskView tests specific to the openstack style urls.
Many of the original TaskView tests are valid and need
DelegateAPI tests specific to the openstack style urls.
Many of the original DelegateAPI tests are valid and need
not be repeated here, but some additional features in the
unique TaskViews need testing.
unique DelegateAPI need testing.
"""
def test_new_user(self):
@ -65,8 +65,8 @@ class OpenstackAPITests(AdjutantAPITestCase):
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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -95,8 +95,8 @@ class OpenstackAPITests(AdjutantAPITestCase):
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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -108,8 +108,8 @@ class OpenstackAPITests(AdjutantAPITestCase):
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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
response = self.client.get(url, headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -310,48 +310,6 @@ class OpenstackAPITests(AdjutantAPITestCase):
if adj_user['id'] == user2.id:
self.assertTrue(adj_user['manageable'])
def test_force_reset_password(self):
"""
Ensure the force password endpoint works as expected,
and only for admin.
Should also check if template can be rendered.
"""
user = fake_clients.FakeUser(
name="test@example.com", password="123", email="test@example.com")
setup_identity_cache(users=[user])
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "_member_",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
url = "/v1/openstack/users/password-set"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
headers["roles"] = "admin,_member_"
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
data = {'password': 'new_test_password'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(user.password, 'new_test_password')
def test_remove_user_role(self):
""" Remove all roles on a user from our project """
project = fake_clients.FakeProject(name="test_project")
@ -382,9 +340,8 @@ class OpenstackAPITests(AdjutantAPITestCase):
data = {'roles': ["_member_"]}
response = self.client.delete(url, data,
format='json', headers=admin_headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json(),
{'notes': ['Task completed successfully.']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
@override_settings(USERNAME_IS_EMAIL=False)
def test_new_user_username_not_email(self):
@ -408,8 +365,8 @@ class OpenstackAPITests(AdjutantAPITestCase):
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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -441,11 +398,14 @@ class QuotaAPITests(AdjutantAPITestCase):
setup_mock_caches('RegionTwo', 'test_project_id')
def check_quota_cache(self, region_name, project_id, size,
extra_services=[]):
extra_services=None):
"""
Helper function to check if the global quota caches now match the size
defined in the config
"""
if extra_services is None:
extra_services = []
cinderquota = cinder_cache[region_name][project_id]['quota']
gigabytes = settings.PROJECT_QUOTA_SIZES[size]['cinder']['gigabytes']
self.assertEqual(cinderquota['gigabytes'], gigabytes)
@ -492,7 +452,7 @@ class QuotaAPITests(AdjutantAPITestCase):
response = self.client.post(url, data,
headers=admin_headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', project.id, 'medium')
@ -527,7 +487,7 @@ class QuotaAPITests(AdjutantAPITestCase):
response = self.client.post(url, data,
headers=admin_headers, format='json')
# First check we can actually access the page correctly
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', project.id, 'medium')
@ -595,7 +555,7 @@ class QuotaAPITests(AdjutantAPITestCase):
response = self.client.post(url, data,
headers=admin_headers, format='json')
# First check we can actually access the page correctly
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', project.id, 'medium')
@ -605,7 +565,7 @@ class QuotaAPITests(AdjutantAPITestCase):
response = self.client.post(url, data,
headers=admin_headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', project.id, 'small')
@ -640,7 +600,7 @@ class QuotaAPITests(AdjutantAPITestCase):
response = self.client.post(url, data,
headers=admin_headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', project.id, 'medium')
@ -655,7 +615,7 @@ class QuotaAPITests(AdjutantAPITestCase):
response = self.client.post(url, data,
headers=admin_headers, format='json')
# First check we can actually access the page correctly
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', project.id, 'small')
@ -694,7 +654,7 @@ class QuotaAPITests(AdjutantAPITestCase):
'regions': ['RegionOne']}
response = self.client.post(url, data, headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', project.id, 'medium')
@ -711,7 +671,7 @@ class QuotaAPITests(AdjutantAPITestCase):
'project_id': project2.id}
response = self.client.post(url, data, headers=headers, format='json')
# First check we can actually access the page correctly
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', project2.id, 'medium')
@ -956,7 +916,7 @@ class QuotaAPITests(AdjutantAPITestCase):
data = {'size': 'medium', 'regions': ['RegionOne', 'RegionTwo']}
response = self.client.post(url, data, headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
@ -990,7 +950,7 @@ class QuotaAPITests(AdjutantAPITestCase):
'regions': ['RegionOne', 'RegionTwo']}
response = self.client.post(url, data, headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.check_quota_cache('RegionOne', project.id, 'medium')
@ -1061,7 +1021,7 @@ class QuotaAPITests(AdjutantAPITestCase):
'regions': ['RegionOne']}
response = self.client.post(url, data, headers=headers, format='json')
# First check we can actually access the page correctly
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.check_quota_cache('RegionOne', project.id, 'medium')
@ -1171,7 +1131,7 @@ class QuotaAPITests(AdjutantAPITestCase):
response = self.client.post(url, data,
headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.check_quota_cache('RegionOne', project.id, 'small')
@ -1319,7 +1279,7 @@ class QuotaAPITests(AdjutantAPITestCase):
response = self.client.post(url, data,
headers=admin_headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Then check to see the quotas have changed
self.check_quota_cache(

View File

@ -20,8 +20,9 @@ from django.core import mail
from rest_framework import status
from adjutant.api.models import Task, Token, Notification
from adjutant.api.v1.tasks import CreateProject
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
@ -31,10 +32,10 @@ from adjutant.common.tests.utils import (AdjutantAPITestCase,
@mock.patch('adjutant.common.user_store.IdentityManager',
FakeManager)
class TaskViewTests(AdjutantAPITestCase):
class DelegateAPITests(AdjutantAPITestCase):
"""
Tests to ensure the approval/token workflow does what is
expected with the given TaskViews. These test don't check
expected with the given DelegateAPIs. These test don't check
final results for actions, simply that the tasks, action,
and tokens are created/updated.
"""
@ -96,11 +97,11 @@ class TaskViewTests(AdjutantAPITestCase):
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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'invite_user')
self.assertEqual(mail.outbox[0].subject, 'invite_user_to_project')
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -195,8 +196,8 @@ class TaskViewTests(AdjutantAPITestCase):
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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -237,10 +238,14 @@ class TaskViewTests(AdjutantAPITestCase):
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_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json(),
{'notes': ['Task completed successfully.']})
{'notes': ['task created']})
tasks = Task.objects.all()
self.assertEqual(1, len(tasks))
self.assertTrue(tasks[0].completed)
def test_new_project(self):
"""
@ -249,10 +254,10 @@ class TaskViewTests(AdjutantAPITestCase):
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -266,7 +271,7 @@ class TaskViewTests(AdjutantAPITestCase):
url = "/v1/tasks/" + new_task.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json(),
{'notes': ['created token']}
@ -289,10 +294,10 @@ class TaskViewTests(AdjutantAPITestCase):
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -306,7 +311,7 @@ class TaskViewTests(AdjutantAPITestCase):
url = "/v1/tasks/" + new_task.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.data,
{'notes': ['created token']}
@ -332,10 +337,10 @@ class TaskViewTests(AdjutantAPITestCase):
setup_identity_cache(projects=[project])
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",
@ -352,8 +357,7 @@ class TaskViewTests(AdjutantAPITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.json(),
{'errors': ['Cannot approve an invalid task. '
'Update data and rerun pre_approve.']})
{'errors': ['actions invalid']})
def test_new_project_existing_user(self):
"""
@ -367,10 +371,10 @@ class TaskViewTests(AdjutantAPITestCase):
setup_identity_cache(users=[user])
# unauthenticated sign up as existing user
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': user.email}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# approve the sign-up as admin
headers = {
@ -398,15 +402,15 @@ class TaskViewTests(AdjutantAPITestCase):
setup_identity_cache()
# create signup#1 - project1 with user 1
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Create signup#2 - project1 with user 2
data = {'project_name': "test_project", 'email': "test2@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "admin_project",
@ -421,7 +425,7 @@ class TaskViewTests(AdjutantAPITestCase):
url = "/v1/tasks/" + new_task1.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json(),
{'notes': ['created token']}
@ -452,7 +456,7 @@ class TaskViewTests(AdjutantAPITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
@ -480,28 +484,26 @@ class TaskViewTests(AdjutantAPITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
# Submit password reset again
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
# Verify the first token doesn't work
first_token = Token.objects.all()[0]
url = "/v1/tokens/" + first_token.token
data = {'password': 'new_test_password1'}
# Submit password reset again
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, 400)
self.assertEqual(user.password, '123')
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
# confirm the old toke has been cleared:
second_token = Token.objects.all()[0]
self.assertNotEqual(first_token.token, second_token.token)
# Now reset with the second token
second_token = Token.objects.all()[1]
url = "/v1/tokens/" + second_token.token
data = {'password': 'new_test_password2'}
response = self.client.post(url, data, format='json')
@ -518,22 +520,24 @@ class TaskViewTests(AdjutantAPITestCase):
url = "/v1/actions/ResetPassword"
data = {'email': "test@exampleinvalid.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
def test_notification_createproject(self):
self.assertFalse(len(Token.objects.all()))
def test_notification_CreateProjectAndUser(self):
"""
CreateProject should create a notification.
CreateProjectAndUser should create a notification.
We should be able to grab it.
"""
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
new_task = Task.objects.all()[0]
@ -560,16 +564,16 @@ class TaskViewTests(AdjutantAPITestCase):
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
data = {'project_name': "test_project_2", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
def test_duplicate_tasks_new_user(self):
"""
@ -591,79 +595,19 @@ class TaskViewTests(AdjutantAPITestCase):
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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
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_"],
'project_id': project.id}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
def test_return_task_id_if_admin(self):
"""
Confirm that the task id is returned when admin.
"""
user = fake_clients.FakeUser(
name="test@example.com", password="123", email="test@example.com")
setup_identity_cache(users=[user])
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "admin,_member_",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# make sure the task is actually valid
new_task = Task.objects.all()[0]
self.assertTrue(all([a.valid for a in new_task.actions]))
self.assertEqual(
response.json()['task'],
new_task.uuid)
def test_return_task_id_if_admin_fail(self):
"""
Confirm that the task id is not returned unless admin.
"""
user = fake_clients.FakeUser(
name="test@example.com", password="123", email="test@example.com")
setup_identity_cache(users=[user])
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "_member_",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
url = "/v1/actions/ResetPassword"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# make sure the task is actually valid
new_task = Task.objects.all()[0]
self.assertTrue(all([a.valid for a in new_task.actions]))
self.assertFalse(response.json().get('task'))
def test_update_email_task(self):
"""
Ensure the update email workflow goes as expected.
@ -688,8 +632,8 @@ class TaskViewTests(AdjutantAPITestCase):
data = {'new_email': "new_test@example.com"}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -700,15 +644,15 @@ class TaskViewTests(AdjutantAPITestCase):
self.assertEqual(user.name, 'new_test@example.com')
@modify_dict_settings(TASK_SETTINGS=[
{'key_list': ['update_email', 'additional_actions'],
{'key_list': ['update_user_email', 'additional_actions'],
'operation': 'append',
'value': ['SendAdditionalEmailAction']},
{'key_list': ['update_email', 'action_settings',
{'key_list': ['update_user_email', 'action_settings',
'SendAdditionalEmailAction', 'initial'],
'operation': 'update',
'value': {
'subject': 'email_update_additional',
'template': 'email_update_started.txt',
'subject': 'update_user_email_additional',
'template': 'update_user_email_started.txt',
'email_roles': [],
'email_current_user': True,
}
@ -738,15 +682,16 @@ class TaskViewTests(AdjutantAPITestCase):
data = {'new_email': "new_test@example.com"}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
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, 'email_update_additional')
self.assertEqual(
mail.outbox[0].subject, 'update_user_email_additional')
self.assertEqual(mail.outbox[1].to, ['new_test@example.com'])
self.assertEqual(mail.outbox[1].subject, 'email_update_token')
self.assertEqual(mail.outbox[1].subject, 'update_user_email_token')
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -759,15 +704,15 @@ class TaskViewTests(AdjutantAPITestCase):
self.assertEqual(len(mail.outbox), 3)
@modify_dict_settings(TASK_SETTINGS=[
{'key_list': ['update_email', 'additional_actions'],
{'key_list': ['update_user_email', 'additional_actions'],
'operation': 'append',
'value': ['SendAdditionalEmailAction']},
{'key_list': ['update_email', 'action_settings',
{'key_list': ['update_user_email', 'action_settings',
'SendAdditionalEmailAction', 'initial'],
'operation': 'update',
'value': {
'subject': 'email_update_additional',
'template': 'email_update_started.txt',
'subject': 'update_user_email_additional',
'template': 'update_user_email_started.txt',
'email_roles': [],
'email_current_user': True}
}
@ -799,15 +744,16 @@ class TaskViewTests(AdjutantAPITestCase):
data = {'new_email': "new_test@example.com"}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
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, 'email_update_additional')
self.assertEqual(
mail.outbox[0].subject, 'update_user_email_additional')
self.assertEqual(mail.outbox[1].to, ['new_test@example.com'])
self.assertEqual(mail.outbox[1].subject, 'email_update_token')
self.assertEqual(mail.outbox[1].subject, 'update_user_email_token')
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -900,8 +846,8 @@ class TaskViewTests(AdjutantAPITestCase):
data = {'new_email': "new_test@example.com"}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
self.assertEqual(len(mail.outbox), 1)
@ -953,8 +899,8 @@ class TaskViewTests(AdjutantAPITestCase):
data = {'new_email': "new_test@example.com"}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -967,7 +913,7 @@ class TaskViewTests(AdjutantAPITestCase):
# Tests for USERNAME_IS_EMAIL=False
@override_settings(USERNAME_IS_EMAIL=False)
def test_invite_user_email_not_username(self):
def test_invite_user_to_project_email_not_username(self):
"""
Invites a user where the email is different to the username.
"""
@ -987,11 +933,11 @@ class TaskViewTests(AdjutantAPITestCase):
data = {'username': 'new_user', 'email': "new@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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'invite_user')
self.assertEqual(mail.outbox[0].subject, 'invite_user_to_project')
self.assertEqual(mail.outbox[0].to[0], 'new@example.com')
new_token = Token.objects.all()[0]
@ -1028,7 +974,7 @@ class TaskViewTests(AdjutantAPITestCase):
# horizon
data = {'email': "test@example.com", 'username': 'test_user'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.json()['notes'],
['If user with email exists, reset token will be issued.'])
@ -1049,16 +995,16 @@ class TaskViewTests(AdjutantAPITestCase):
def test_new_project_username_not_email(self):
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com",
'username': 'test'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
data = {'email': "new_test@example.com", 'username': "new",
'project_name': 'new_project'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
new_task = Task.objects.all()[0]
@ -1075,7 +1021,7 @@ class TaskViewTests(AdjutantAPITestCase):
}
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -1086,15 +1032,15 @@ class TaskViewTests(AdjutantAPITestCase):
@modify_dict_settings(
TASK_SETTINGS=[
{'key_list': ['invite_user', 'additional_actions'],
{'key_list': ['invite_user_to_project', 'additional_actions'],
'operation': 'append',
'value': ['SendAdditionalEmailAction']},
{'key_list': ['invite_user', 'action_settings',
{'key_list': ['invite_user_to_project', 'action_settings',
'SendAdditionalEmailAction', 'initial'],
'operation': 'update',
'value': {
'subject': 'email_update_additional',
'template': 'email_update_started.txt',
'subject': 'update_user_email_additional',
'template': 'update_user_email_started.txt',
'email_roles': ['project_admin'],
'email_current_user': False,
}
@ -1173,15 +1119,16 @@ class TaskViewTests(AdjutantAPITestCase):
data = {'email': "new_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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
self.assertEqual(len(mail.outbox), 2)
self.assertEqual(len(mail.outbox[0].to), 2)
self.assertEqual(set(mail.outbox[0].to),
set([user.email, user2.email]))
self.assertEqual(mail.outbox[0].subject, 'email_update_additional')
self.assertEqual(
mail.outbox[0].subject, 'update_user_email_additional')
# Test that the token email gets sent to the other addresses
self.assertEqual(mail.outbox[1].to[0], 'new_test@example.com')
@ -1195,15 +1142,15 @@ class TaskViewTests(AdjutantAPITestCase):
@modify_dict_settings(
TASK_SETTINGS=[
{'key_list': ['invite_user', 'additional_actions'],
{'key_list': ['invite_user_to_project', 'additional_actions'],
'operation': 'append',
'value': ['SendAdditionalEmailAction']},
{'key_list': ['invite_user', 'action_settings',
{'key_list': ['invite_user_to_project', 'action_settings',
'SendAdditionalEmailAction', 'initial'],
'operation': 'update',
'value': {
'subject': 'email_update_additional',
'template': 'email_update_started.txt',
'subject': 'update_user_email_additional',
'template': 'update_user_email_started.txt',
'email_roles': ['project_admin'],
'email_current_user': False,
}
@ -1242,8 +1189,8 @@ class TaskViewTests(AdjutantAPITestCase):
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_200_OK)
self.assertEqual(response.data, {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data, {'notes': ['task created']})
self.assertEqual(len(mail.outbox), 1)
@ -1259,15 +1206,15 @@ class TaskViewTests(AdjutantAPITestCase):
@modify_dict_settings(
TASK_SETTINGS=[
{'key_list': ['invite_user', 'additional_actions'],
{'key_list': ['invite_user_to_project', 'additional_actions'],
'operation': 'override',
'value': ['SendAdditionalEmailAction']},
{'key_list': ['invite_user', 'action_settings',
{'key_list': ['invite_user_to_project', 'action_settings',
'SendAdditionalEmailAction', 'initial'],
'operation': 'update',
'value':{
'subject': 'invite_user_additional',
'template': 'email_update_started.txt',
'subject': 'invite_user_to_project_additional',
'template': 'update_user_email_started.txt',
'email_additional_addresses': ['admin@example.com'],
'email_current_user': False,
}
@ -1312,14 +1259,15 @@ class TaskViewTests(AdjutantAPITestCase):
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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
self.assertEqual(len(mail.outbox), 2)
self.assertEqual(set(mail.outbox[0].to),
set(['admin@example.com']))
self.assertEqual(mail.outbox[0].subject, 'invite_user_additional')
self.assertEqual(
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')
@ -1332,15 +1280,15 @@ class TaskViewTests(AdjutantAPITestCase):
@modify_dict_settings(
TASK_SETTINGS=[
{'key_list': ['invite_user', 'additional_actions'],
{'key_list': ['invite_user_to_project', 'additional_actions'],
'operation': 'override',
'value': ['SendAdditionalEmailAction']},
{'key_list': ['invite_user', 'action_settings',
{'key_list': ['invite_user_to_project', 'action_settings',
'SendAdditionalEmailAction', 'initial'],
'operation': 'update',
'value':{
'subject': 'invite_user_additional',
'template': 'email_update_started.txt',
'subject': 'invite_user_to_project_additional',
'template': 'update_user_email_started.txt',
'email_additional_addresses': ['admin@example.com'],
'email_current_user': False,
}
@ -1373,29 +1321,29 @@ class TaskViewTests(AdjutantAPITestCase):
@mock.patch('adjutant.common.tests.fake_clients.FakeManager.find_project')
def test_all_actions_setup(self, mocked_find):
"""
Ensures that all actions have been setup before pre_approve is
run on any actions, even if we have a pre_approve failure.
Ensures that all actions have been setup before prepare is
run on any actions, even if we have a prepare failure.
Deals with: bug/1745053
"""
setup_identity_cache()
mocked_find.side_effect = KeyError()
mocked_find.side_effect = KeyError("Error forced for testing")
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(
response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
new_task = Task.objects.all()[0]
class_conf = settings.TASK_SETTINGS.get(
CreateProject.task_type, settings.DEFAULT_TASK_SETTINGS)
CreateProjectAndUser.task_type, settings.DEFAULT_TASK_SETTINGS)
expected_action_names = (
class_conf.get('default_actions', [])
or CreateProject.default_actions[:])
or CreateProjectAndUser.default_actions[:])
expected_action_names += class_conf.get('additional_actions', [])
actions = new_task.actions
@ -1410,18 +1358,17 @@ class TaskViewTests(AdjutantAPITestCase):
setup_identity_cache()
mocked_find.side_effect = KeyError("Forced key error.")
mocked_find.side_effect = KeyError("Error forced for testing")
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(
response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
self.assertEqual(
response.json(),
{'errors': ["Error: Something went wrong on the server. "
"It will be looked into shortly."]})
{'errors': ['Service temporarily unavailable, try again later.']})
new_task = Task.objects.all()[0]
new_notification = Notification.objects.all()[0]
@ -1430,7 +1377,7 @@ class TaskViewTests(AdjutantAPITestCase):
self.assertEqual(
new_notification.notes,
{'errors': [
"Error: KeyError('Forced key error.') while setting up "
"Error: KeyError('Error forced for testing') while setting up "
"task. See task itself for details."]})
self.assertEqual(new_notification.task, new_task)
@ -1484,8 +1431,8 @@ class TaskViewTests(AdjutantAPITestCase):
data = {'username': 'new_user', '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_200_OK)
self.assertEqual(response.json(), {'notes': ['created token']})
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
@override_settings(KEYSTONE={'can_edit_users': False})
def test_project_create_cant_edit_users(self):
@ -1499,10 +1446,10 @@ class TaskViewTests(AdjutantAPITestCase):
"""
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
task = Task.objects.all()[0]
action_models = task.actions
@ -1523,10 +1470,10 @@ class TaskViewTests(AdjutantAPITestCase):
setup_identity_cache(users=[user])
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.json(), {'notes': ['task created']})
task = Task.objects.all()[0]
action_models = task.actions

View File

@ -28,9 +28,9 @@ urlpatterns = [
url(r'^notifications/?$', views.NotificationList.as_view()),
]
for active_view in settings.ACTIVE_TASKVIEWS:
taskview = settings.TASKVIEW_CLASSES[active_view]
for active_view in settings.ACTIVE_DELEGATE_APIS:
delegate_api = settings.DELEGATE_API_CLASSES[active_view]
urlpatterns.append(
url(taskview['url'], taskview['class'].as_view())
url(delegate_api['url'], delegate_api['class'].as_view())
)

View File

@ -12,150 +12,19 @@
# License for the specific language governing permissions and limitations
# under the License.
import hashlib
import json
from datetime import timedelta
from smtplib import SMTPException
from uuid import uuid4
from decorator import decorator
from django.conf import settings
from django.core.exceptions import FieldError
from django.core.mail import EmailMultiAlternatives
from django.template import loader
from django.utils import timezone
from rest_framework.response import Response
from adjutant.api.models import Notification, Token
def create_token(task):
expire = timezone.now() + timedelta(hours=settings.TOKEN_EXPIRE_TIME)
uuid = uuid4().hex
token = Token.objects.create(
task=task,
token=uuid,
expires=expire
)
token.save()
return token
def send_stage_email(task, email_conf, token=None):
if not email_conf:
return
text_template = loader.get_template(
email_conf['template'],
using='include_etc_templates')
html_template = email_conf.get('html_template', None)
if html_template:
html_template = loader.get_template(
html_template,
using='include_etc_templates')
emails = set()
actions = {}
# find our set of emails and actions that require email
for action in task.actions:
act = action.get_action()
email = act.get_email()
if email:
emails.add(email)
actions[str(act)] = act
if not emails:
return
if len(emails) > 1:
notes = {
'errors':
("Error: Unable to send update, more than one email for task: %s"
% task.uuid)
}
create_notification(task, notes, error=True)
return
context = {
'task': task,
'actions': actions
}
if token:
if settings.HORIZON_URL:
tokenurl = settings.HORIZON_URL
if not tokenurl.endswith('/'):
tokenurl += '/'
tokenurl += 'token/'
else:
tokenurl = settings.TOKEN_SUBMISSION_URL
if not tokenurl.endswith('/'):
tokenurl += '/'
context.update({
'tokenurl': tokenurl,
'token': token.token
})
try:
message = text_template.render(context)
# from_email is the return-path and is distinct from the
# message headers
from_email = email_conf.get('from')
if not from_email:
from_email = email_conf['reply']
elif "%(task_uuid)s" in from_email:
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,
# From needs to be set to be disctinct from return-path
'From': email_conf['reply'],
'Reply-To': email_conf['reply'],
}
email = EmailMultiAlternatives(
email_conf['subject'],
message,
from_email,
[emails.pop()],
headers=headers,
)
if html_template:
email.attach_alternative(
html_template.render(context), "text/html")
email.send(fail_silently=False)
except SMTPException 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", {})
if errors_conf:
notification = create_notification(
task, notes, error=True,
engines=errors_conf.get('engines', True))
if errors_conf.get('notification') == "acknowledge":
notification.acknowledged = True
notification.save()
else:
create_notification(task, notes, error=True)
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,
@ -186,28 +55,6 @@ def create_notification(task, notes, error=False, engines=True):
return notification
def create_task_hash(task_type, action_list):
hashable_list = [task_type, ]
for action in action_list:
hashable_list.append(action['name'])
if not action['serializer']:
continue
# iterate like this to maintain consistent order for hash
fields = sorted(action['serializer'].validated_data.keys())
for field in fields:
try:
hashable_list.append(
action['serializer'].validated_data[field])
except KeyError:
if field == "username" and settings.USERNAME_IS_EMAIL:
continue
else:
raise
return hashlib.sha256(str(hashable_list).encode('utf-8')).hexdigest()
# "{'filters': {'fieldname': { 'operation': 'value'}}
@decorator
def parse_filters(func, *args, **kwargs):

View File

@ -14,7 +14,6 @@
from logging import getLogger
from django.conf import settings
from django.utils import timezone
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
@ -24,9 +23,11 @@ from rest_framework.views import APIView
from adjutant.api import utils
from adjutant.api.views import SingleVersionView
from adjutant.api.models import Notification, Task, Token
from adjutant.api.v1.utils import (
create_notification, create_token, parse_filters, send_stage_email)
from adjutant.api.models import Notification, Token
from adjutant.api.v1.utils import parse_filters
from adjutant import exceptions
from adjutant.tasks.v1.manager import TaskManager
from adjutant.tasks.models import Task
class V1VersionEndpoint(SingleVersionView):
@ -40,29 +41,7 @@ class APIViewWithLogger(APIView):
def __init__(self, *args, **kwargs):
super(APIViewWithLogger, self).__init__(*args, **kwargs)
self.logger = getLogger('adjutant')
def _handle_task_error(self, e, task, error_text="while running task",
return_response=False):
import traceback
trace = traceback.format_exc()
self.logger.critical((
"(%s) - Exception escaped! %s\nTrace: \n%s") % (
timezone.now(), e, trace))
notes = {
'errors':
["Error: %s(%s) %s. See task itself for details."
% (type(e).__name__, e, error_text)]
}
create_notification(task, notes, error=True)
response_dict = {
'errors':
["Error: Something went wrong on the server. "
"It will be looked into shortly."]
}
if return_response:
return Response(response_dict, status=500)
return response_dict, 500
self.task_manager = TaskManager()
class StatusView(APIViewWithLogger):
@ -219,6 +198,7 @@ class TaskList(APIViewWithLogger):
if not filters:
filters = {}
# TODO(adriant): better handle this bit of incode policy
if 'admin' not in request.keystone_user['roles']:
# Ignore any filters with project_id in them
for field_filter in filters.keys():
@ -240,7 +220,7 @@ class TaskList(APIViewWithLogger):
status=400)
task_list = []
for task in tasks:
task_list.append(task._to_dict())
task_list.append(task.to_dict())
if tasks_per_page:
return Response({'tasks': task_list,
@ -262,13 +242,13 @@ class TaskDetail(APIViewWithLogger):
and its related actions.
"""
try:
# TODO(adriant): better handle this bit of incode policy
if 'admin' in request.keystone_user['roles']:
task = Task.objects.get(uuid=uuid)
return Response(task._to_dict())
else:
task = Task.objects.get(
uuid=uuid, project_id=request.keystone_user['project_id'])
return Response(task.to_dict())
return Response(task.to_dict())
except Task.DoesNotExist:
return Response(
{'errors': ['No task with this id.']},
@ -278,200 +258,37 @@ class TaskDetail(APIViewWithLogger):
def put(self, request, uuid, format=None):
"""
Allows the updating of action data and retriggering
of the pre_approve step.
of the prepare step.
"""
try:
task = Task.objects.get(uuid=uuid)
except Task.DoesNotExist:
return Response(
{'errors': ['No task with this id.']},
status=404)
self.task_manager.update(uuid, request.data)
if task.completed:
return Response(
{'errors':
['This task has already been completed.']},
status=400)
if task.cancelled:
# NOTE(adriant): If we can uncancel a task, that should happen
# at this endpoint.
return Response(
{'errors':
['This task has been cancelled.']},
status=400)
if task.approved:
return Response(
{'errors':
['This task has already been approved.']},
status=400)
act_list = []
valid = True
for action in task.actions:
action_serializer = settings.ACTION_CLASSES[action.action_name][1]
if action_serializer is not None:
serializer = action_serializer(data=request.data)
else:
serializer = None
act_list.append({
'name': action.action_name,
'action': action,
'serializer': serializer})
if serializer is not None and not serializer.is_valid():
valid = False
if valid:
for act in act_list:
if act['serializer'] is not None:
data = act['serializer'].validated_data
else:
data = {}
act['action'].action_data = data
act['action'].save()
try:
act['action'].get_action().pre_approve()
except Exception as e:
return self._handle_task_error(
e, task, "while updating task", return_response=True)
return Response(
{'notes': ["Task successfully updated."]},
status=200)
else:
errors = {}
for act in act_list:
if act['serializer'] is not None:
errors.update(act['serializer'].errors)
return Response({'errors': errors}, status=400)
return Response(
{'notes': ["Task successfully updated."]},
status=200)
@utils.admin
def post(self, request, uuid, format=None):
"""
Will approve the Task specified,
followed by running the post_approve actions
followed by running the approve actions
and if valid will setup and create a related token.
"""
try:
task = Task.objects.get(uuid=uuid)
except Task.DoesNotExist:
return Response(
{'errors': ['No task with this id.']},
status=404)
try:
if request.data.get('approved') is not True:
return Response(
{'approved': ["this is a required boolean field."]},
status=400)
raise exceptions.TaskSerializersInvalid(
{'approved': ["this is a required boolean field."]})
except ParseError:
return Response(
{'approved': ["this is a required boolean field."]},
status=400)
raise exceptions.TaskSerializersInvalid(
{'approved': ["this is a required boolean field."]})
task = self.task_manager.approve(uuid, request.keystone_user)
if task.completed:
return Response(
{'errors':
['This task has already been completed.']},
status=400)
if task.cancelled:
{'notes': ["Task completed successfully."]}, status=200)
else:
return Response(
{'errors':
['This task has been cancelled.']},
status=400)
# we check that the task is valid before approving it:
valid = True
for action in task.actions:
if not action.valid:
valid = False
if not valid:
return Response(
{'errors':
['Cannot approve an invalid task. '
'Update data and rerun pre_approve.']},
status=400)
if task.approved:
# Expire previously in use tokens
Token.objects.filter(task=task.uuid).delete()
# We approve the task before running actions,
# that way if something goes wrong we know if it was approved,
# when it was approved, and who approved it last. Subsequent
# reapproval attempts overwrite previous approved_by/on.
task.approved = True
task.approved_by = request.keystone_user
task.approved_on = timezone.now()
task.save()
need_token = False
valid = True
actions = []
for action in task.actions:
act_model = action.get_action()
actions.append(act_model)
try:
act_model.post_approve()
except Exception as e:
return self._handle_task_error(
e, task, "while approving task", return_response=True)
if not action.valid:
valid = False
if action.need_token:
need_token = True
if valid:
if need_token:
token = create_token(task)
try:
class_conf = settings.TASK_SETTINGS.get(
task.task_type, settings.DEFAULT_TASK_SETTINGS)
# will throw a key error if the token template has not
# been specified
email_conf = class_conf['emails']['token']
send_stage_email(task, email_conf, token)
return Response({'notes': ['created token']},
status=200)
except KeyError as e:
return self._handle_task_error(
e, task, "while sending token", return_response=True)
else:
for action in actions:
try:
action.submit({})
except Exception as e:
return self._handle_task_error(
e, task, "while submitting task",
return_response=True)
task.completed = True
task.completed_on = timezone.now()
task.save()
# Sending confirmation email:
class_conf = settings.TASK_SETTINGS.get(
task.task_type, settings.DEFAULT_TASK_SETTINGS)
email_conf = class_conf.get(
'emails', {}).get('completed', None)
send_stage_email(task, email_conf)
return Response(
{'notes': ["Task completed successfully."]},
status=200)
return Response({'errors': ['actions invalid']}, status=400)
{'notes': ['created token']}, status=202)
@utils.mod_or_admin
def delete(self, request, uuid, format=None):
@ -482,6 +299,7 @@ class TaskDetail(APIViewWithLogger):
associated with their project.
"""
try:
# TODO(adriant): better handle this bit of incode policy
if 'admin' in request.keystone_user['roles']:
task = Task.objects.get(uuid=uuid)
else:
@ -492,20 +310,7 @@ class TaskDetail(APIViewWithLogger):
{'errors': ['No task with this id.']},
status=404)
if task.completed:
return Response(
{'errors':
['This task has already been completed.']},
status=400)
if task.cancelled:
return Response(
{'errors':
['This task has already been cancelled.']},
status=400)
task.cancelled = True
task.save()
self.task_manager.cancel(task)
return Response(
{'notes': ["Task cancelled successfully."]},
@ -545,6 +350,7 @@ class TokenList(APIViewWithLogger):
{'errors': {'task': ["This field is required.", ]}},
status=400)
try:
# TODO(adriant): better handle this bit of incode policy
if 'admin' in request.keystone_user['roles']:
task = Task.objects.get(uuid=uuid)
else:
@ -555,38 +361,7 @@ class TokenList(APIViewWithLogger):
{'errors': ['No task with this id.']},
status=404)
if task.completed:
return Response(
{'errors':
['This task has already been completed.']},
status=400)
if task.cancelled:
return Response(
{'errors':
['This task has been cancelled.']},
status=400)
if not task.approved:
return Response(
{'errors': ['This task has not been approved.']},
status=400)
for token in task.tokens:
token.delete()
token = create_token(task)
try:
class_conf = settings.TASK_SETTINGS.get(
task.task_type, settings.DEFAULT_TASK_SETTINGS)
# will throw a key error if the token template has not
# been specified
email_conf = class_conf['emails']['token']
send_stage_email(task, email_conf, token)
except KeyError as e:
return self._handle_task_error(
e, task, "while sending token", return_response=True)
self.task_manager.reissue_token(task)
return Response(
{'notes': ['Token reissued.']}, status=200)
@ -660,69 +435,7 @@ class TokenDetail(APIViewWithLogger):
{'errors': ['This token does not exist or has expired.']},
status=404)
if token.task.completed:
return Response(
{'errors':
['This task has already been completed.']},
status=400)
if token.task.cancelled:
return Response(
{'errors':
['This task has been cancelled.']},
status=400)
required_fields = set()
actions = []
for action in token.task.actions:
a = action.get_action()
actions.append(a)
for field in a.token_fields:
required_fields.add(field)
errors = {}
data = {}
for field in required_fields:
try:
data[field] = request.data[field]
except KeyError:
errors[field] = ["This field is required.", ]
except TypeError:
errors = ["Improperly formated json. "
"Should be a key-value object."]
break
if errors:
return Response({"errors": errors}, status=400)
valid = True
for action in actions:
try:
action.submit(data)
if not action.valid:
valid = False
except Exception as e:
return self._handle_task_error(
e, token.task, "while submiting task",
return_response=True)
if not valid:
return Response({"errors": ["Actions invalid"]}, status=400)
token.task.completed = True
token.task.completed_on = timezone.now()
token.task.save()
token.delete()
# Sending confirmation email:
class_conf = settings.TASK_SETTINGS.get(
token.task.task_type, settings.DEFAULT_TASK_SETTINGS)
email_conf = class_conf.get(
'emails', {}).get('completed', None)
send_stage_email(token.task, email_conf)
self.task_manager.submit(token.task, request.data)
return Response(
{'notes': ["Token submitted successfully."]},

View File

@ -99,7 +99,10 @@ class FakeRoleAssignment(object):
def setup_identity_cache(projects=None, users=None, role_assignments=None,
credentials=None, extra_roles=[]):
credentials=None, extra_roles=None):
if extra_roles is None:
extra_roles = []
if not projects:
projects = []
if not users:

View File

@ -78,33 +78,33 @@ class ModifySettingsTests(AdjutantAPITestCase):
admin_data = {'email': 'admin@example.com'}
override = {
'key_list': ['reset_password', 'action_settings',
'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_200_OK)
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_200_OK)
self.assertEqual(1, Token.objects.count())
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_200_OK)
self.assertEqual(2, Token.objects.count())
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_200_OK)
self.assertEqual(3, Token.objects.count())
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_200_OK)
self.assertEqual(3, Token.objects.count())
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(1, Token.objects.count())
def test_modify_settings_remove_password(self):
"""
@ -130,26 +130,26 @@ class ModifySettingsTests(AdjutantAPITestCase):
data = {'email': 'admin@example.com'}
override = {
'key_list': ['reset_password', 'action_settings',
'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_200_OK)
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_200_OK)
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_200_OK)
self.assertEqual(1, Token.objects.count())
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(0, Token.objects.count())
@modify_dict_settings(TASK_SETTINGS={
'key_list': ['reset_password', 'action_settings',
'key_list': ['reset_user_password', 'action_settings',
'ResetUserPasswordAction', 'blacklisted_roles'],
'operation': 'append',
'value': ['test_role']})
@ -191,12 +191,12 @@ class ModifySettingsTests(AdjutantAPITestCase):
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
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_200_OK)
self.assertEqual(response2.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(0, Token.objects.count())
def test_modify_settings_update_email(self):
@ -232,16 +232,16 @@ class ModifySettingsTests(AdjutantAPITestCase):
}
override = [
{'key_list': ['update_email', 'emails', 'token'],
{'key_list': ['update_user_email', 'emails', 'token'],
'operation': 'update',
'value': {
'subject': 'modified_token_email',
'template': 'email_update_token.txt'}
'template': 'update_user_email_token.txt'}
}
]
response = self.client.post(url, data, headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(len(mail.outbox), 1)
self.assertNotEqual(mail.outbox[0].subject, 'modified_token_email')
@ -250,14 +250,14 @@ class ModifySettingsTests(AdjutantAPITestCase):
response = self.client.post(url, data,
headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
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_200_OK)
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

@ -36,10 +36,13 @@ def get_managable_roles(user_roles):
return managable_role_names
def subtree_ids_list(subtree, id_list=[]):
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.iterkeys():
for key in subtree.keys():
id_list.append(key)
if subtree[key]:
subtree_ids_list(subtree[key], id_list)

View File

@ -12,27 +12,138 @@
# License for the specific language governing permissions and limitations
# under the License.
from rest_framework import status
class BaseServiceException(Exception):
"""Configuration, or core service logic has had an error
This is an internal only exception and should only be thrown
when and error occurs that the user shouldn't see.
If thrown during the course of an API call will be caught and returned
to the user as an ServiceUnavailable error with a 503 response.
"""
default_message = "A internal service error has occured."
class BaseException(Exception):
"""An error occurred."""
def __init__(self, message=None):
self.message = message
def __str__(self):
return self.message or self.__class__.__doc__
return self.message or self.default_message
class TaskViewNotFound(BaseException):
"""Attempting to setup TaskView that has not been registered."""
class InvalidActionClass(BaseServiceException):
default_message = (
"Cannot register action not built off the BaseAction class.")
class ActionNotFound(BaseException):
"""Attempting to setup Action that has not been registered."""
class InvalidActionSerializer(BaseServiceException):
default_message = (
"Action serializer must be a valid DRF serializer.")
class SerializerMissingException(BaseException):
""" Serializer configured but it does not exist """
class InvalidTaskClass(BaseServiceException):
default_message = (
"Action serializer must be a valid DRF serializer.")
class ConfirmationException(BaseException):
""" Missing or incorrect configuration value. """
class InvalidAPIClass(BaseServiceException):
default_message = (
"Cannot register task not built off the BaseTask class.")
class DelegateAPINotRegistered(BaseServiceException):
default_message = (
"Failed to setup DelegateAPI that has not been registered.")
class TaskNotRegistered(BaseServiceException):
default_message = "Failed to setup Task that has not been registered."
class ActionNotRegistered(BaseServiceException):
default_message = "Failed to setup Action that has not been registered."
class SerializerMissingException(BaseServiceException):
default_message = "Serializer configured but it does not exist."
class ConfigurationException(BaseServiceException):
default_message = "Missing or incorrect configuration value."
class BaseAPIException(Exception):
"""An Task error occurred."""
status_code = status.HTTP_400_BAD_REQUEST
def __init__(self, message=None, internal_message=None):
if message:
self.message = message
else:
self.message = self.default_message
self.internal_message = internal_message
def __str__(self):
message = ""
if self.internal_message:
message = "%s - " % self.internal_message
message += str(self.message)
return message
class NotFound(BaseAPIException):
status_code = status.HTTP_404_NOT_FOUND
default_message = 'Not found.'
class TaskNotFound(NotFound):
status_code = status.HTTP_404_NOT_FOUND
default_message = 'Task not found.'
class ServiceUnavailable(BaseAPIException):
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
default_message = 'Service temporarily unavailable, try again later.'
class TaskSerializersInvalid(BaseAPIException):
default_message = "Data passed to the Task was invalid."
class TaskDuplicateFound(BaseAPIException):
default_message = "This Task already exists."
status_code = status.HTTP_409_CONFLICT
class BaseTaskException(BaseAPIException):
default_message = "An Task error occurred."
status_code = status.HTTP_400_BAD_REQUEST
def __init__(self, task, message=None, internal_message=None):
super(BaseTaskException, self).__init__(message, internal_message)
self.task = task
def __str__(self):
message = "%s (%s) - " % (self.task.task_type, self.task.uuid)
message += super(BaseTaskException, self).__str__()
return message
class TaskTokenSerializersInvalid(BaseTaskException):
default_message = "Data passed for the Task token was invalid."
class TaskActionsInvalid(BaseTaskException):
default_message = "One or more of the Task actions was invalid."
class TaskStateInvalid(BaseTaskException):
default_message = "Action does is not possible on task in current state."
class TaskActionsFailed(BaseTaskException):
"""For use when Task processing fails and we want to wrap that."""
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
default_message = 'Service temporarily unavailable, try again later.'

View File

@ -43,7 +43,7 @@ class EmailNotification(NotificationEngine):
send an email with the given templates.
Example conf:
<TaskView>:
<task_type>:
notifications:
EmailNotification:
standard:

View File

@ -6,7 +6,6 @@ There is a task that needs some attention.
Related Task:
uuid: {{ task.uuid }}
ip_address: {{ task.ip_address }}
keystone_user: {{ task.keystone_user|safe }}
project_id: {{ task.project_id }}
task_type: {{ task.task_type }}

View File

@ -55,10 +55,10 @@ class NotificationTests(AdjutantAPITestCase):
setup_identity_cache()
url = "/v1/actions/CreateProject"
url = "/v1/actions/CreateProjectAndUser"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
headers = {
'project_name': "test_project",

View File

@ -27,7 +27,7 @@ import os
import sys
import yaml
from adjutant.utils import setup_task_settings
from adjutant.exceptions import ConfirmationException
from adjutant.exceptions import ConfigurationException
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Application definition
@ -44,6 +44,7 @@ INSTALLED_APPS = (
'adjutant.actions',
'adjutant.api',
'adjutant.notifications',
'adjutant.tasks',
)
MIDDLEWARE_CLASSES = (
@ -94,6 +95,17 @@ TEMPLATES = [
},
]
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'adjutant.api.exception_handler.exception_handler',
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
],
'DEFAULT_PERMISSION_CLASSES': [],
}
# Setup of local settings data
if 'test' in sys.argv:
from adjutant import test_settings
@ -112,12 +124,9 @@ SECRET_KEY = CONFIG['SECRET_KEY']
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.get('DEBUG', False)
if not DEBUG:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
}
if DEBUG:
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append(
'rest_framework.renderers.BrowsableAPIRenderer')
ALLOWED_HOSTS = CONFIG.get('ALLOWED_HOSTS', [])
@ -158,7 +167,7 @@ if TOKEN_SUBMISSION_URL:
HORIZON_URL = CONFIG.get('HORIZON_URL')
if not HORIZON_URL and not TOKEN_SUBMISSION_URL:
raise ConfirmationException("Must supply 'HORIZON_URL'")
raise ConfigurationException("Must supply 'HORIZON_URL'")
TOKEN_EXPIRE_TIME = CONFIG['TOKEN_EXPIRE_TIME']
@ -181,14 +190,12 @@ PROJECT_QUOTA_SIZES = CONFIG.get('PROJECT_QUOTA_SIZES')
QUOTA_SIZES_ASC = CONFIG.get('QUOTA_SIZES_ASC', [])
# Defaults for backwards compatibility.
ACTIVE_TASKVIEWS = CONFIG.get(
'ACTIVE_TASKVIEWS',
ACTIVE_DELEGATE_APIS = CONFIG.get(
'ACTIVE_DELEGATE_APIS',
[
'UserRoles',
'UserDetail',
'UserResetPassword',
'UserSetPassword',
'UserList',
'RoleList'
])
@ -199,12 +206,14 @@ QUOTA_SERVICES = CONFIG.get(
{'*': ['cinder', 'neutron', 'nova']})
# Dict of TaskViews and their url_paths.
# - This is populated by registering taskviews.
TASKVIEW_CLASSES = {}
# 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 = {}

View File

@ -1,33 +1,29 @@
from django.apps import AppConfig
from django.conf import settings
from adjutant.exceptions import ActionNotFound, TaskViewNotFound
from adjutant.exceptions import ActionNotRegistered, DelegateAPINotRegistered
def check_expected_taskviews():
expected_taskviews = settings.ACTIVE_TASKVIEWS
def check_expected_delegate_apis():
missing_delegate_apis = list(
set(settings.ACTIVE_DELEGATE_APIS)
- set(settings.DELEGATE_API_CLASSES.keys()))
missing_taskviews = list(
set(expected_taskviews) - set(settings.TASKVIEW_CLASSES.keys()))
if missing_taskviews:
raise TaskViewNotFound(
if missing_delegate_apis:
raise DelegateAPINotRegistered(
message=(
"Expected taskviews are unregistered: %s" % missing_taskviews))
"Expected DelegateAPIs are unregistered: %s"
% missing_delegate_apis))
def check_configured_actions():
"""Check that all the expected actions have been registered."""
configured_actions = []
for taskview in settings.ACTIVE_TASKVIEWS:
task_class = settings.TASKVIEW_CLASSES.get(taskview)['class']
for task in settings.TASK_CLASSES:
task_class = settings.TASK_CLASSES.get(task)
try:
configured_actions += settings.TASK_SETTINGS.get(
task_class.task_type, {})['default_actions']
except KeyError:
configured_actions += task_class.default_actions
configured_actions += task_class.default_actions
configured_actions += settings.TASK_SETTINGS.get(
task_class.task_type, {}).get('additional_actions', [])
@ -35,7 +31,7 @@ def check_configured_actions():
set(configured_actions) - set(settings.ACTION_CLASSES.keys()))
if missing_actions:
raise ActionNotFound(
raise ActionNotRegistered(
"Configured actions are unregistered: %s" % missing_actions)
@ -51,8 +47,8 @@ class StartUpConfig(AppConfig):
Useful for any start up checks.
"""
# First check that all expect taskviews are present
check_expected_taskviews()
# First check that all expect DelegateAPIs are present
check_expected_delegate_apis()
# Now check if all the actions those views expecte are present.
check_configured_actions()

View File

View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.21 on 2019-06-10 02:09
from __future__ import unicode_literals
import adjutant.tasks.models
from django.db import migrations, models
import django.utils.timezone
import jsonfield.fields
class Migration(migrations.Migration):
dependencies = [
('api', '0005_auto_20190610_0209'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.CreateModel(
name='Task',
fields=[
('uuid', models.CharField(default=adjutant.tasks.models.hex_uuid, max_length=32, primary_key=True, serialize=False)),
('hash_key', models.CharField(db_index=True, max_length=64)),
('ip_address', models.GenericIPAddressField()),
('keystone_user', jsonfield.fields.JSONField(default={})),
('project_id', models.CharField(db_index=True, max_length=64, null=True)),
('approved_by', jsonfield.fields.JSONField(default={})),
('task_type', models.CharField(db_index=True, max_length=100)),
('action_notes', jsonfield.fields.JSONField(default={})),
('cancelled', models.BooleanField(db_index=True, default=False)),
('approved', models.BooleanField(db_index=True, default=False)),
('completed', models.BooleanField(db_index=True, default=False)),
('created_on', models.DateTimeField(default=django.utils.timezone.now)),
('approved_on', models.DateTimeField(null=True)),
('completed_on', models.DateTimeField(null=True)),
],
options={
'indexes': [],
},
),
],
),
]

View File

@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.21 on 2019-06-19 06:13
from __future__ import unicode_literals
from django.db import migrations, models
import jsonfield.fields
class Migration(migrations.Migration):
dependencies = [
('tasks', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='task',
name='ip_address',
),
migrations.AddField(
model_name='task',
name='task_notes',
field=jsonfield.fields.JSONField(default=[]),
),
migrations.AlterField(
model_name='task',
name='approved',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='task',
name='cancelled',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='task',
name='completed',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='task',
name='hash_key',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='task',
name='project_id',
field=models.CharField(max_length=64, null=True),
),
migrations.AlterField(
model_name='task',
name='task_type',
field=models.CharField(max_length=100),
),
migrations.AddIndex(
model_name='task',
index=models.Index(fields=['completed'], name='completed_idx'),
),
migrations.AddIndex(
model_name='task',
index=models.Index(fields=['project_id', 'uuid'], name='tasks_task_project_a1cfa7_idx'),
),
migrations.AddIndex(
model_name='task',
index=models.Index(fields=['project_id', 'task_type'], name='tasks_task_project_e86456_idx'),
),
migrations.AddIndex(
model_name='task',
index=models.Index(fields=['project_id', 'task_type', 'cancelled'], name='tasks_task_project_f0ec0e_idx'),
),
migrations.AddIndex(
model_name='task',
index=models.Index(fields=['project_id', 'task_type', 'completed', 'cancelled'], name='tasks_task_project_1cb2a8_idx'),
),
migrations.AddIndex(
model_name='task',
index=models.Index(fields=['hash_key', 'completed', 'cancelled'], name='tasks_task_hash_ke_781b6a_idx'),
),
]

View File

127
adjutant/tasks/models.py Normal file
View File

@ -0,0 +1,127 @@
# 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 django.conf import settings
from django.db import models
from uuid import uuid4
from django.utils import timezone
from jsonfield import JSONField
def hex_uuid():
return uuid4().hex
class Task(models.Model):
"""
Wrapper object for the request and related actions.
Stores the state of the Task and a log for the
action.
"""
uuid = models.CharField(max_length=32, default=hex_uuid,
primary_key=True)
hash_key = models.CharField(max_length=64)
# who is this:
keystone_user = JSONField(default={})
project_id = models.CharField(max_length=64, null=True)
# keystone_user for the approver:
approved_by = JSONField(default={})
# type of the task, for easy grouping
task_type = models.CharField(max_length=100)
# task level notes
task_notes = JSONField(default=[])
# Effectively a log of what the actions are doing.
action_notes = JSONField(default={})
cancelled = models.BooleanField(default=False)
approved = models.BooleanField(default=False)
completed = models.BooleanField(default=False)
created_on = models.DateTimeField(default=timezone.now)
approved_on = models.DateTimeField(null=True)
completed_on = models.DateTimeField(null=True)
class Meta:
indexes = [
models.Index(fields=['completed'], name='completed_idx'),
models.Index(fields=['project_id', 'uuid']),
models.Index(fields=['project_id', 'task_type']),
models.Index(fields=['project_id', 'task_type', 'cancelled']),
models.Index(fields=[
'project_id', 'task_type', 'completed', 'cancelled']),
models.Index(fields=['hash_key', 'completed', 'cancelled']),
]
def __init__(self, *args, **kwargs):
super(Task, self).__init__(*args, **kwargs)
# in memory dict to be used for passing data between actions:
self.cache = {}
def get_task(self):
"""Returns self as the appropriate task wrapper type."""
return settings.TASK_CLASSES[self.task_type](task_model=self)
@property
def actions(self):
return self.action_set.order_by('order')
@property
def tokens(self):
return self.token_set.all()
@property
def notifications(self):
return self.notification_set.all()
def to_dict(self):
actions = []
for action in self.actions:
actions.append({
"action_name": action.action_name,
"data": action.action_data,
"valid": action.valid
})
return {
"uuid": self.uuid,
"keystone_user": self.keystone_user,
"approved_by": self.approved_by,
"project_id": self.project_id,
"actions": actions,
"task_type": self.task_type,
"task_notes": self.task_notes,
"action_notes": self.action_notes,
"cancelled": self.cancelled,
"approved": self.approved,
"completed": self.completed,
"created_on": self.created_on,
"approved_on": self.approved_on,
"completed_on": self.completed_on,
}
def add_task_note(self, note):
self.task_notes.append(note)
self.save()
def add_action_note(self, action, note):
if action in self.action_notes:
self.action_notes[action].append(note)
else:
self.action_notes[action] = [note]
self.save()

View File

@ -0,0 +1 @@
default_app_config = 'adjutant.tasks.v1.app.TasksV1Config'

7
adjutant/tasks/v1/app.py Normal file
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class TasksV1Config(AppConfig):
name = "adjutant.tasks.v1"
label = 'tasks_v1'

438
adjutant/tasks/v1/base.py Normal file
View File

@ -0,0 +1,438 @@
# 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 hashlib
from logging import getLogger
from django.conf import settings
from adjutant.api.models import Task
from django.utils import timezone
from adjutant.api.v1.utils import create_notification
from adjutant.tasks.v1.utils import (
send_stage_email, create_token, handle_task_error)
from adjutant import exceptions
class BaseTask(object):
"""
Base class for in memory task representation.
This serves as the internal task logic handler, and is used to
define what a task looks like.
Most of the time this class shouldn't be called or used directly
as the task manager is what handles the direct interaction to the
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
deprecated_task_types = None
def __init__(self,
task_model=None,
task_data=None,
action_data=None):
self.logger = getLogger('adjutant')
if task_model:
self.task = task_model
self._refresh_actions()
else:
# raises 400 validation error
action_serializer_list = self._instantiate_action_serializers(
action_data)
hash_key = self._create_task_hash(action_serializer_list)
# raises duplicate error
self._handle_duplicates(hash_key)
keystone_user = task_data.get('keystone_user', {})
self.task = Task.objects.create(
keystone_user=keystone_user,
project_id=keystone_user.get('project_id'),
task_type=self.task_type,
hash_key=hash_key)
self.task.save()
# Instantiate actions with serializers
self.actions = []
for i, action in enumerate(action_serializer_list):
data = action['serializer'].validated_data
# construct the action class
self.actions.append(action['action'](
data=data,
task=self.task,
order=i
))
self.logger.info(
"(%s) - '%s' task created (%s)."
% (timezone.now(), self.task_type, self.task.uuid))
def _instantiate_action_serializers(self, action_data,
use_existing_actions=False):
action_serializer_list = []
if use_existing_actions:
actions = self.actions
else:
actions = self.default_actions[:]
actions += self.settings.get('additional_actions', [])
# instantiate all action serializers and check validity
valid = True
for action in actions:
if use_existing_actions:
action_name = action.action.action_name
else:
action_name = action
action_class, serializer_class = \
settings.ACTION_CLASSES[action_name]
if use_existing_actions:
action_class = action
# instantiate serializer class
if not serializer_class:
raise exceptions.SerializerMissingException(
"No serializer defined for action %s" % action_name)
serializer = serializer_class(data=action_data)
action_serializer_list.append({
'name': action_name,
'action': action_class,
'serializer': serializer})
if serializer and not serializer.is_valid():
valid = False
if not valid:
errors = {}
for action in action_serializer_list:
if action['serializer']:
errors.update(action['serializer'].errors)
raise exceptions.TaskSerializersInvalid(errors)
return action_serializer_list
def _create_task_hash(self, action_list):
hashable_list = [self.task_type, ]
for action in action_list:
hashable_list.append(action['name'])
if not action['serializer']:
continue
# iterate like this to maintain consistent order for hash
fields = sorted(action['serializer'].validated_data.keys())
for field in fields:
try:
hashable_list.append(
action['serializer'].validated_data[field])
except KeyError:
if field == "username" and settings.USERNAME_IS_EMAIL:
continue
else:
raise
return hashlib.sha256(str(hashable_list).encode('utf-8')).hexdigest()
def _handle_duplicates(self, hash_key):
duplicate_tasks = Task.objects.filter(
hash_key=hash_key,
completed=0,
cancelled=0)
if not duplicate_tasks:
return
if self.duplicate_policy == "cancel":
now = timezone.now()
self.logger.info(
"(%s) - Task is a duplicate - Cancelling old tasks." %
now)
for task in duplicate_tasks:
task.add_task_note(
"Task cancelled because was an old duplicate. - (%s)"
% now)
task.get_task().cancel()
return
raise exceptions.TaskDuplicateFound()
def _refresh_actions(self):
self.actions = [a.get_action() for a in self.task.actions]
def _create_token(self):
self.clear_tokens()
token = create_token(self.task)
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']
send_stage_email(self.task, email_conf, token)
except KeyError as e:
handle_task_error(e, self.task, error_text='while sending token')
def add_note(self, note):
"""
Logs the note, and also adds it to the task notes.
"""
now = timezone.now()
self.logger.info(
"(%s)(%s)(%s) - %s" % (now, self.task_type, self.task.uuid, note))
note = "%s - (%s)" % (note, now)
self.task.add_task_note(note)
@property
def settings(self):
"""Get my settings.
Returns a dict of the settings for this task.
"""
try:
return settings.TASK_SETTINGS[self.task_type]
except KeyError:
return settings.DEFAULT_TASK_SETTINGS
def is_valid(self, internal_message=None):
self._refresh_actions()
valid = all([act.valid for act in self.actions])
if not valid:
# TODO(amelia): get action invalidation reasons and raise those
raise exceptions.TaskActionsInvalid(
self.task, 'actions invalid', internal_message)
@property
def approved(self):
return self.task.approved
@property
def completed(self):
return self.task.completed
@property
def cancelled(self):
return self.task.cancelled
def confirm_state(self, approved=None, completed=None, cancelled=None):
"""Check that the Task is in a given state.
None value means state is ignored. Otherwise expects true or false.
"""
if completed is not None:
if self.task.completed and not completed:
raise exceptions.TaskStateInvalid(
self.task, "This task has already been completed.")
if not self.task.completed and completed:
raise exceptions.TaskStateInvalid(
self.task, "This task hasn't been completed.")
if cancelled is not None:
if self.task.cancelled and not cancelled:
raise exceptions.TaskStateInvalid(
self.task, "This task has been cancelled.")
if not self.task.cancelled and cancelled:
raise exceptions.TaskStateInvalid(
self.task, "This task has not been cancelled.")
if approved is not None:
if self.task.approved and not approved:
raise exceptions.TaskStateInvalid(
self.task, "This task has already been approved.")
if not self.task.approved and approved:
raise exceptions.TaskStateInvalid(
self.task, "This task has not been approved.")
def update(self, action_data):
self.confirm_state(approved=False, completed=False, cancelled=False)
action_serializer_list = self._instantiate_action_serializers(
action_data, use_existing_actions=True)
hash_key = self._create_task_hash(action_serializer_list)
self._handle_duplicates(hash_key)
for action in action_serializer_list:
data = action['serializer'].validated_data
action['action'].action.action_data = data
action['action'].action.save()
self._refresh_actions()
self.prepare()
def prepare(self):
"""Run the prepare stage for all the actions.
If the task can be auto approved, this will also run the approve
stage.
"""
self.confirm_state(approved=False, completed=False, cancelled=False)
for action in self.actions:
try:
action.prepare()
except Exception as e:
handle_task_error(
e, self.task, error_text='while setting up task')
# send initial confirmation email:
email_conf = self.settings.get('emails', {}).get('initial', None)
send_stage_email(self.task, email_conf)
approve_list = [act.auto_approve for act in self.actions]
# TODO(amelia): It would be nice to explicitly test this, however
# currently we don't have the right combinations of
# actions to allow for it.
if False in approve_list:
can_auto_approve = False
elif True in approve_list:
can_auto_approve = True
else:
can_auto_approve = False
if self.settings.get('allow_auto_approve') is not None:
allow_auto_approve = self.settings.get('allow_auto_approve')
else:
allow_auto_approve = self.allow_auto_approve
if can_auto_approve and not allow_auto_approve:
self.add_note("Actions allow auto aproval, but task does not.")
elif can_auto_approve:
self.add_note("Action allow auto approval. Auto approving.")
self.approve()
return
if self.send_approval_notification:
notes = {
'notes':
["'%s' task needs approval." % self.task_type]
}
create_notification(self.task, notes)
def approve(self, approved_by="system"):
"""Run the approve stage for all the actions.
"""
self.confirm_state(completed=False, cancelled=False)
self.is_valid("task invalid before approval")
# We approve the task before running actions,
# that way if something goes wrong we know if it was approved,
# when it was approved, and who approved it.
self.task.approved = True
self.task.approved_on = timezone.now()
self.task.approved_by = approved_by
self.task.save()
# approve all actions
for action in self.actions:
try:
action.approve()
except Exception as e:
handle_task_error(
e, self.task, error_text='while approving task')
self.is_valid("task invalid after approval")
need_token = any([act.need_token for act in self.actions])
if need_token:
self._create_token()
else:
self.submit()
def reissue_token(self):
self.confirm_state(approved=True, completed=False, cancelled=False)
need_token = any([act.need_token for act in self.actions])
if need_token:
self._create_token()
def clear_tokens(self):
for token in self.task.tokens:
token.delete()
def submit(self, token_data=None):
self.confirm_state(approved=True, completed=False, cancelled=False)
required_fields = set()
actions = []
for action in self.task.actions:
a = action.get_action()
actions.append(a)
for field in a.token_fields:
required_fields.add(field)
if not token_data:
token_data = {}
errors = {}
data = {}
for field in required_fields:
try:
data[field] = token_data[field]
except KeyError:
errors[field] = ["This field is required.", ]
except TypeError:
errors = ["Improperly formated json. "
"Should be a key-value object."]
break
if errors:
raise exceptions.TaskTokenSerializersInvalid(self.task, errors)
self.is_valid("task invalid before submit")
for action in actions:
try:
action.submit(data)
except Exception as e:
handle_task_error(
e, self.task, "while submiting task")
self.is_valid("task invalid after submit")
self.task.completed = True
self.task.completed_on = timezone.now()
self.task.save()
for token in self.task.tokens:
token.delete()
# Sending confirmation email:
email_conf = self.settings.get(
'emails', {}).get('completed', None)
send_stage_email(self.task, email_conf)
def cancel(self):
self.confirm_state(completed=False, cancelled=False)
self.clear_tokens()
self.task.cancelled = True
self.task.save()

View File

@ -0,0 +1,107 @@
# 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.
from logging import getLogger
from six import string_types
from django.conf import settings
from adjutant import exceptions
from adjutant.tasks.models import Task
from adjutant.tasks.v1.base import BaseTask
class TaskManager(object):
def __init__(self, message=None):
self.logger = getLogger('adjutant')
def _get_task_class(self, task_type):
"""Get the task class from the given task_type
If the task_type is a string, it will get the correct class,
otherwise if it is a valid task class, will return it.
"""
try:
return settings.TASK_CLASSES[task_type]
except KeyError:
if task_type in settings.TASK_CLASSES.values():
return task_type
raise exceptions.TaskNotRegistered(
"Unknown task type: '%s'" % task_type)
def create_from_request(self, task_type, request):
task_class = self._get_task_class(task_type)
task_data = {
"keystone_user": request.keystone_user,
"project_id": request.keystone_user.get("project_id"),
}
task = task_class(task_data=task_data, action_data=request.data)
task.prepare()
return task
def create_from_data(self, task_type, task_data, action_data):
task_class = self._get_task_class(task_type)
task = task_class(task_data=task_data, action_data=action_data)
task.prepare()
return task
def get(self, task):
if isinstance(task, BaseTask):
return task
if isinstance(task, string_types):
try:
task = Task.objects.get(uuid=task)
except Task.DoesNotExist:
raise exceptions.TaskNotFound(
"Task not found with uuid of: '%s'" % task)
if isinstance(task, Task):
try:
return settings.TASK_CLASSES[task.task_type](task)
except KeyError:
# TODO(adriant): Maybe we should handle this better
# for older deprecated tasks:
raise exceptions.TaskNotRegistered(
"Task type '%s' not registered, "
"and used for existing task."
% task.task_type
)
raise exceptions.TaskNotFound(
"Task not found for value of: '%s'" % task)
def update(self, task, action_data):
task = self.get(task)
task.update(action_data)
return task
def approve(self, task, approved_by):
task = self.get(task)
task.approve(approved_by)
return task
def submit(self, task, token_data):
task = self.get(task)
task.submit(token_data)
return task
def cancel(self, task):
task = self.get(task)
task.cancel()
return task
def reissue_token(self, task):
task = self.get(task)
task.reissue_token()
return task

View File

@ -0,0 +1,43 @@
# Copyright (C) 2019 Catalyst Cloud Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import settings
from adjutant import exceptions
from adjutant.tasks.v1.base import BaseTask
from adjutant.tasks.v1 import projects, users, resources
def register_task_class(task_class):
if not issubclass(task_class, BaseTask):
raise exceptions.InvalidTaskClass(
"'%s' is not a built off the BaseTask class."
% task_class.__name__
)
data = {}
data[task_class.task_type] = task_class
if task_class.deprecated_task_types:
for old_type in task_class.deprecated_task_types:
data[old_type] = task_class
settings.TASK_CLASSES.update(data)
register_task_class(projects.CreateProjectAndUser)
register_task_class(users.EditUserRoles)
register_task_class(users.InviteUser)
register_task_class(users.ResetUserPassword)
register_task_class(users.UpdateUserEmail)
register_task_class(resources.UpdateProjectQuotas)

View File

@ -0,0 +1,24 @@
# 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.tasks.v1.base import BaseTask
class CreateProjectAndUser(BaseTask):
duplicate_policy = "block"
task_type = "create_project_and_user"
deprecated_task_types = ['create_project']
default_actions = [
"NewProjectWithUserAction",
]

View File

@ -0,0 +1,22 @@
# 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.tasks.v1.base import BaseTask
class UpdateProjectQuotas(BaseTask):
task_type = "update_quota"
default_actions = [
"UpdateProjectQuotasAction",
]

View File

@ -0,0 +1,48 @@
# 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.tasks.v1.base import BaseTask
class InviteUser(BaseTask):
duplicate_policy = "block"
task_type = "invite_user_to_project"
deprecated_task_types = ['invite_user']
default_actions = [
"NewUserAction",
]
class ResetUserPassword(BaseTask):
task_type = "reset_user_password"
deprecated_task_types = ['reset_password']
default_actions = [
"ResetUserPasswordAction",
]
class EditUserRoles(BaseTask):
task_type = "edit_user_roles"
deprecated_task_types = ['edit_user']
default_actions = [
"EditUserRolesAction",
]
class UpdateUserEmail(BaseTask):
task_type = "update_user_email"
deprecated_task_types = ['update_email']
default_actions = [
"UpdateUserEmailAction",
]

168
adjutant/tasks/v1/utils.py Normal file
View File

@ -0,0 +1,168 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from logging import getLogger
from 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
LOG = getLogger('adjutant')
def handle_task_error(e, task, error_text="while running task"):
import traceback
trace = traceback.format_exc()
LOG.critical((
"(%s) - Exception escaped! %s\nTrace: \n%s") % (
timezone.now(), e, trace))
notes = ["Error: %s(%s) %s. See task itself for details."
% (type(e).__name__, e, error_text)]
raise exceptions.TaskActionsFailed(task, internal_message=notes)
def create_token(task):
expire = timezone.now() + timedelta(hours=settings.TOKEN_EXPIRE_TIME)
uuid = uuid4().hex
token = Token.objects.create(
task=task,
token=uuid,
expires=expire
)
token.save()
return token
def send_stage_email(task, email_conf, token=None):
if not email_conf:
return
text_template = loader.get_template(
email_conf['template'],
using='include_etc_templates')
html_template = email_conf.get('html_template', None)
if html_template:
html_template = loader.get_template(
html_template,
using='include_etc_templates')
emails = set()
actions = {}
# find our set of emails and actions that require email
for action in task.actions:
act = action.get_action()
email = act.get_email()
if email:
emails.add(email)
actions[str(act)] = act
if not emails:
return
if len(emails) > 1:
notes = {
'errors':
("Error: Unable to send update, more than one email for task: %s"
% task.uuid)
}
create_notification(task, notes, error=True)
return
context = {
'task': task,
'actions': actions
}
if token:
if settings.HORIZON_URL:
tokenurl = settings.HORIZON_URL
if not tokenurl.endswith('/'):
tokenurl += '/'
tokenurl += 'token/'
else:
tokenurl = settings.TOKEN_SUBMISSION_URL
if not tokenurl.endswith('/'):
tokenurl += '/'
context.update({
'tokenurl': tokenurl,
'token': token.token
})
try:
message = text_template.render(context)
# from_email is the return-path and is distinct from the
# message headers
from_email = email_conf.get('from')
if not from_email:
from_email = email_conf['reply']
elif "%(task_uuid)s" in from_email:
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,
# From needs to be set to be disctinct from return-path
'From': email_conf['reply'],
'Reply-To': email_conf['reply'],
}
email = EmailMultiAlternatives(
email_conf['subject'],
message,
from_email,
[emails.pop()],
headers=headers,
)
if html_template:
email.attach_alternative(
html_template.render(context), "text/html")
email.send(fail_silently=False)
except SMTPException 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", {})
if errors_conf:
notification = create_notification(
task, notes, error=True,
engines=errors_conf.get('engines', True))
if errors_conf.get('notification') == "acknowledge":
notification.acknowledged = True
notification.save()
else:
create_notification(task, notes, error=True)

View File

@ -17,6 +17,7 @@ SECRET_KEY = '+er!4olta#17a=n%uotcazg2ncpl==yjog%1*o-(cr%zys-)!'
ADDITIONAL_APPS = [
'adjutant.api.v1',
'adjutant.actions.v1',
'adjutant.tasks.v1',
]
DATABASES = {
@ -75,14 +76,13 @@ HORIZON_URL = 'http://localhost:8080/'
TOKEN_EXPIRE_TIME = 24
ACTIVE_TASKVIEWS = [
ACTIVE_DELEGATE_APIS = [
'UserRoles',
'UserDetail',
'UserResetPassword',
'UserSetPassword',
'UserList',
'RoleList',
'CreateProject',
'CreateProjectAndUser',
'InviteUser',
'ResetPassword',
'EditUser',
@ -188,31 +188,31 @@ DEFAULT_ACTION_SETTINGS = {
}
TASK_SETTINGS = {
'invite_user': {
'invite_user_to_project': {
'emails': {
'initial': None,
'token': {
'template': 'invite_user_token.txt',
'subject': 'invite_user'
'template': 'invite_user_to_project_token.txt',
'subject': 'invite_user_to_project'
},
'completed': {
'template': 'invite_user_completed.txt',
'subject': 'invite_user'
'template': 'invite_user_to_project_completed.txt',
'subject': 'invite_user_to_project'
}
}
},
'create_project': {
'create_project_and_user': {
'emails': {
'initial': {
'template': 'signup_initial.txt',
'template': 'create_project_and_user_initial.txt',
'subject': 'signup received'
},
'token': {
'template': 'signup_token.txt',
'template': 'create_project_and_user_token.txt',
'subject': 'signup approved'
},
'completed': {
'template': 'signup_completed.txt',
'template': 'create_project_and_user_completed.txt',
'subject': 'signup completed'
}
},
@ -223,47 +223,34 @@ TASK_SETTINGS = {
'default_region': 'RegionOne',
'default_parent_id': None,
},
'reset_password': {
'reset_user_password': {
'duplicate_policy': 'cancel',
'emails': {
'initial': None,
'token': {
'template': 'password_reset_token.txt',
'template': 'reset_user_password_token.txt',
'subject': 'Password Reset for OpenStack'
},
'completed': {
'template': 'password_reset_completed.txt',
'template': 'reset_user_password_completed.txt',
'subject': 'Password Reset for OpenStack'
}
}
},
'force_password': {
'duplicate_policy': 'cancel',
'emails': {
'token': {
'template': 'initial_password_token.txt',
'subject': 'Setup Your OpenStack Password'
},
'completed': {
'template': 'initial_password_completed.txt',
'subject': 'Setup Your OpenStack Password'
}
}
},
'update_email': {
'update_user_email': {
'emails': {
'initial': None,
'token': {
'subject': 'email_update_token',
'template': 'email_update_token.txt'
'subject': 'update_user_email_token',
'template': 'update_user_email_token.txt'
},
'completed': {
'subject': 'Email Update Complete',
'template': 'email_update_completed.txt'
'template': 'update_user_email_completed.txt'
}
},
},
'edit_user': {
'edit_user_roles': {
'role_blacklist': ['admin']
},
'update_quota': {
@ -420,7 +407,7 @@ conf_dict = {
"EMAIL_SETTINGS": EMAIL_SETTINGS,
"USERNAME_IS_EMAIL": USERNAME_IS_EMAIL,
"KEYSTONE": KEYSTONE,
"ACTIVE_TASKVIEWS": ACTIVE_TASKVIEWS,
"ACTIVE_DELEGATE_APIS": ACTIVE_DELEGATE_APIS,
"DEFAULT_TASK_SETTINGS": DEFAULT_TASK_SETTINGS,
"TASK_SETTINGS": TASK_SETTINGS,
"DEFAULT_ACTION_SETTINGS": DEFAULT_ACTION_SETTINGS,

View File

@ -76,7 +76,7 @@ Response Example
"ip_address": "127.0.0.1",
"keystone_user": {},
"project_id": null,
"task_type": "reset_password",
"task_type": "reset_user_password",
"uuid": "d5c7901cfecd45ec9a87871035c9f662"
},
{
@ -120,7 +120,7 @@ Response Example
"ip_address": "127.0.0.1",
"keystone_user": {},
"project_id": null,
"task_type": "signup",
"task_type": "create_project_and_user",
"uuid": "370d952c63ba410c8704abc12cfd97b7"
}
}
@ -178,7 +178,7 @@ Response Example
"ip_address": "127.0.0.1",
"keystone_user": {},
"project_id": null,
"task_type": "reset_password",
"task_type": "reset_user_password",
"uuid": "d5c7901cfecd45ec9a87871035c9f662"
}
@ -362,7 +362,7 @@ Response Example
"required_fields": [
"password"
],
"task_type": "signup"
"task_type": "create_project_and_user"
}

View File

@ -1,6 +1,6 @@
**********************************
OpenStack Style TaskView Endpoints
**********************************
*************************************
OpenStack Style DelegateAPI Endpoints
*************************************
A response of 'task created' means that the task requires admin approval and
a response of 'created token' indicates that the task has been auto-approved

View File

@ -3,7 +3,7 @@ Admin Logic Version 1 API reference
###################################
This is the reference for Adjutant when it is using the default configuration.
Different deployments may exclude certain task views or include their own
Different deployments may exclude certain DelegateAPIs or include their own
additional ones.
The core functionality of Adjutant is built around the concept of tasks and
@ -74,14 +74,13 @@ Version One Details Endpoint
Unauthenticated.
Details V1 version details and the available taskviews and their fields.
See below for further details on the individual taskviews.
Details V1 version details.
Normal response code: 200
.. include:: admin-api.inc
.. include:: taskviews.inc
.. include:: delegate-apis.inc
****************************

View File

@ -8,6 +8,7 @@ ALLOWED_HOSTS:
ADDITIONAL_APPS:
- adjutant.api.v1
- adjutant.tasks.v1
- adjutant.actions.v1
DATABASES:
@ -62,11 +63,10 @@ HORIZON_URL: http://localhost:8080/
# time for the token to expire in hours
TOKEN_EXPIRE_TIME: 24
ACTIVE_TASKVIEWS:
ACTIVE_DELEGATE_APIS:
- UserRoles
- UserDetail
- UserResetPassword
- UserSetPassword
- UserList
- RoleList
- SignUp
@ -208,14 +208,8 @@ DEFAULT_ACTION_SETTINGS:
# These are cascading overrides for the default settings:
TASK_SETTINGS:
signup:
# You can override 'default_actions' if needed for given taskviews
# The order of the actions is order of execution.
#
# default_actions:
# - NewProjectAction
#
# Additional actions for views
create_project_and_user:
# Additional actions for task
# These will run after the default actions, in the given order.
additional_actions:
- NewProjectDefaultNetworkAction
@ -223,13 +217,13 @@ TASK_SETTINGS:
emails:
initial:
subject: Your OpenStack signup has been received
template: signup_initial.txt
template: create_project_and_user_initial.txt
token:
subject: Your OpenStack signup has been approved
template: signup_token.txt
template: create_project_and_user_token.txt
completed:
subject: Your OpenStack signup has been completed
template: signup_completed.txt
template: create_project_and_user_completed.txt
notifications:
EmailNotification:
standard:
@ -242,42 +236,32 @@ TASK_SETTINGS:
# 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:
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_token.txt
template: invite_user_to_project_token.txt
completed:
subject: Invitation Completed
template: invite_user_completed.txt
template: invite_user_to_project_completed.txt
errors:
SMTPException:
notification: acknowledge
engines: False
reset_password:
reset_user_password:
duplicate_policy: cancel
emails:
initial: null
token:
subject: Password Reset for OpenStack
template: password_reset_token.txt
template: reset_user_password_token.txt
completed:
subject: Password Reset Completed
template: password_reset_completed.txt
force_password:
duplicate_policy: cancel
emails:
initial: null
token:
subject: Set your OpenStack password
template: initial_password_token.txt
completed:
subject: Welcome to OpenStack!
template: initial_password_completed.txt
edit_user:
template: reset_user_password_completed.txt
edit_user_roles:
duplicate_policy: cancel
emails:
initial: null
@ -289,7 +273,7 @@ TASK_SETTINGS:
emails:
initial: null
token: null
update_email:
update_user_email:
duplicate_policy: cancel
additional_actions:
- SendAdditionalEmailAction
@ -297,15 +281,15 @@ TASK_SETTINGS:
initial: null
token:
subject: Confirm OpenStack Email Update
template: email_update_token.txt
template: update_user_email_token.txt
completed:
subject: OpenStack Email Updated
template: email_update_completed.txt
template: update_user_email_completed.txt
action_settings:
SendAdditionalEmailAction:
initial:
subject: OpenStack Email Update Requested
template: email_update_started.txt
template: update_user_email_started.txt
email_current_user: True
update_quota:
duplicate_policy: cancel
@ -315,7 +299,7 @@ TASK_SETTINGS:
token: null
completed:
subject: Openstack Quota updated
template: quota_completed.txt
template: update_quota_completed.txt
# mapping between roles and managable roles
ROLES_MAPPING:

View File

@ -21,6 +21,7 @@ The first part of the configuration file contains standard Django settings.
ADDITIONAL_APPS:
- adjutant.api.v1
- adjutant.tasks.v1
- adjutant.actions.v1
DATABASES:
@ -29,6 +30,7 @@ The first part of the configuration file contains standard Django settings.
NAME: db.sqlite3
LOGGING:
...
EMAIL_SETTINGS:
EMAIL_BACKEND: django.core.mail.backends.console.EmailBackend
@ -37,8 +39,11 @@ The first part of the configuration file contains standard Django settings.
If you have any plugins, ensure that they are also added to
**ADDITIONAL_APPS**.
The next part of the confirguration file contains a number of settings for all
taskviews.
API Settings
------------
The next part of the confirguration file contains a number of settings
for all APIs.
.. code-block:: yaml
@ -51,9 +56,9 @@ taskviews.
auth_url: http://localhost:5000/v3
domain_id: default
TOKEN_SUBMISSION_URL: http://192.168.122.160:8080/token/
HORIZON_URL: http://192.168.122.160:8080/token/
# time for the token to expire in hours
# default time for the token to expire in hours
TOKEN_EXPIRE_TIME: 24
ROLES_MAPPING:
@ -70,6 +75,15 @@ taskviews.
- heat_stack_owner
- _member_
ACTIVE_DELEGATE_APIS:
- UserRoles
- UserDetail
- UserResetPassword
- UserList
- RoleList
- SignUp
- UserUpdateEmail
**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.
@ -84,25 +98,16 @@ should point to that.
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
----------------------
.. code-block:: yaml
ACTIVE_TASKVIEWS:
- UserRoles
- UserDetail
- UserResetPassword
- UserSetPassword
- UserList
- RoleList
- SignUp
- UserUpdateEmail
All in use taskviews, 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.
The DelegateAPIs are built around the task layer, and the tasks themselves
have their own configuration.
.. code-block:: yaml
@ -129,18 +134,19 @@ accessable however users who have started tasks will still be able submit them.
# html_template: completed.txt
error:
The default settings can be 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
**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:
signup:
create_project_and_user:
default_actions:
- NewProjectAction
invite_user:
invite_user_to_project:
additional_actions:
- SendAdditionalEmailAction
@ -218,7 +224,7 @@ 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 it's related task view.
for its related task.
.. code-block:: yaml
@ -228,7 +234,7 @@ for it's related task view.
SendAdditionalEmailAction:
initial:
subject: OpenStack Email Update Requested
template: email_update_started.txt
template: update_user_email_started.txt
email_current_user: True
The additional email action can also send to a subset of people.

View File

@ -12,7 +12,7 @@ type.
An Action is both a simple database representation of itself, and a more
complex in memory class that handles all the logic around it.
Each action class has the functions "pre_approve", "post_approve", and
Each action class has the functions "prepare", "approve", and
"submit". These relate to stages of the approval process, and any python code
can be executed in those functions, some of which should ideally be validation.
@ -24,11 +24,16 @@ requires some info passed along to a later step of execution.
See ``actions.models`` and ``actions.v1`` for a good idea of Actions.
Tasks originate at a TaskView, and start the action processing. They encompass
the user side of interaction.
Tasks, like actions, are also a database representation, and a more complex in
memory class. These classes define what actions the task has, and certain other
elements of how it functions. Most of the logic for task and action processing
is in the base task class, with most interactions with tasks occuring via the
TaskManager.
See ``tasks.models`` and ``tasks.v1`` for a good idea of Tasks.
The main workflow consists of three possible steps which can be executed at
different points in time, depending on how the TaskView and the actions within
different points in time, depending on how the task and the actions within
it are defined.
The base use case is three stages:
@ -36,16 +41,16 @@ The base use case is three stages:
* Receive Request
* Validate request data against action serializers.
* If valid, setup Task to represent the request, and the Actions specified
for that TaskView.
* The service runs the pre_approve function on all actions which should do
for that Task.
* The service runs the "prepare" function on all actions which should do
any self validation to mark the actions themselves as valid or invalid,
and populating the nodes in the Task based on that.
* Admin Approval
* An admin looks at the Task and its notes.
* If they decide it is safe to approve, they do so.
* If there are any invalid actions approval will do nothing until the
action data is updated and initial validation is rerun.
* The service runs the post_approve function on all actions.
and populating the notes in the Task based on that.
* Auto or Admin Approval
* Either a task is set to auto_approve or and admin looks at it to decide.
* If they decide it is safe to approve, they do so.
* If there are any invalid actions approval will do nothing until
the action data is updated and initial validation is rerun.
* The service runs the "approve" function on all actions.
* If any of the actions require a Token to be issued and emailed for
additional data such as a user password, then that will occur.
* If no Token is required, the Task will run submit actions, and be
@ -57,17 +62,17 @@ The base use case is three stages:
* The action will then complete with the given final data.
* Task is marked as complete.
There are cases and TaskViews that auto-approve, and thus automatically do the
There are cases where Tasks auto-approve, and thus automatically do the
middle step right after the first. There are also others which do not need a
Token and thus run the submit step as part of the second, or even all three at
once. The exact number of 'steps' and the time between them depends on the
definition of the TaskView.
definition of the Task.
Actions themselves can also effectively do anything within the scope of those
three stages, and there is even the ability to chain multiple actions together,
and pass data along to other actions.
Details for adding taskviews and actions can be found on the :doc:`plugins`
Details for adding task and actions can be found on the :doc:`plugins`
page.
@ -82,16 +87,19 @@ type.
An Action is both a simple database representation of itself, and a more
complex in memory class that handles all the logic around it.
Each action class has the functions "pre_approve", "post_approve", and
Each action class has the functions "prepare", "approve", and
"submit". These relate to stages of the approval process, and any python code
can be executed in those functions.
What is a Task?
===============
A task is a top level model representation of the request. It wraps the
request metadata, and based on the TaskView, will have actions associated with
it.
A task is a top level model representation of the workflow. Much like an Action
it is a simple database representation of itself, and a more complex in memory
class that handles all the logic around it.
Tasks define what actions are part of a task, and handle the logic of
processing them.
What is a Token?
================
@ -99,27 +107,26 @@ What is a Token?
A token is a unique identifier linking to a task, so that anyone submitting
the token will submit to the actions related to the task.
What is an TaskView?
====================
What is a DelegateAPI?
======================
TaskViews are classes which extend the base TaskView class and use its inbuilt
functions to process actions. They also have actions associated with them and
the inbuilt functions from the base class are there to process and validate
those against data coming in.
DelegateAPIs are classes which extend the base DelegateAPI class.
The TaskView will process incoming data and build it into a Task,
and the related Action classes.
They are mostly used to expose underlying tasks as APIs, and they do so
by using the TaskManager to handle the workflow. The TaskManager will
handle the data validation, and raise error responses for errors that
the user should see. If valid the TaskManager will process incoming
data and build it into a Task, and the related Action classes.
The base TaskView class has three functions:
DelegateAPIs can also be used for small arbitary queries, or building
a full suite of query and task APIs. They are built to be flexible, and
easily pluggable into Adjutant. At their base DelegateAPIs are Django
Rest Framework ApiViews, with a helpers for task handling.
* get
* just a basic view function that by default returns list of actions,
and their required fields for the action view.
* process_actions
* needs to be called in the TaskView definition
* A function to run the processing and validation of request data for
actions.
* Builds and returns the task object, or the validation errors.
At their base TaskViews are django-rest ApiViews, with a few magic functions
to wrap the task logic.
The only constraint with DelegateAPIs is that they should not do any
resourse creation/alteration/deletion themselves. If you need to work with
resources, use the task layer and define a task and actions for it.
Building DelegateAPIs which just query other APIs and don't alter
resources, but need to return information about resources in other
systems is fine. These are useful small little APIs to suppliment any
admin logic you need to expose.

View File

@ -15,7 +15,7 @@ reused in deployer specific workflow in their own plugins. If anything could
be considered a feature, it potentially could be these. The plan is to add many
of these, which any cloud can use out of the box, or augment as needed.
To enable these they must be added to `ACTIVE_TASKVIEWS` in the conf file.
To enable these they must be added to `ACTIVE_DELEGATE_APIS` in the conf file.
For most of these there are matching panels in Horizon.

View File

@ -65,7 +65,7 @@ To run just action unit tests::
To run a single api test::
tox adjutant.api.v1.tests.test_api_taskview.TaskViewTests.test_duplicate_tasks_new_user
tox adjutant.api.v1.tests.test_delegate_api.DelegateAPITests.test_duplicate_tasks_new_user
Tox will run the tests in Python 2.7, Python 3.5 and produce a coverage report.

View File

@ -11,89 +11,102 @@ extend the core service where and when need.
An example of such a plugin is here:
https://github.com/catalyst/adjutant-odoo
New TaskViews should inherit from adjutant.api.v1.tasks.TaskView
Building DelegateAPIs
=====================
New DelegateAPIs should inherit from adjutant.api.v1.base.BaseDelegateAPI
can be registered as such::
from adjutant.api.v1.models import register_taskview_class,
from adjutant.api.v1.models import register_delegate_api_class,
from myplugin import tasks
from myplugin import apis
register_taskview_class(r'^openstack/sign-up/?$', tasks.OpenStackSignUp)
register_delegate_api_class(r'^my-plugin/some-action/?$', apis.MyAPIView)
A DelegateAPI must both be registered with a valid URL and specified in
ACTIVE_DELEGATE_APIS in the configuration to be accessible.
A new DelegateAPI from a plugin can effectively 'override' a default
DelegateAPI by registering with the same URL. However it must have
a different class name and the previous DelegateAPI must be removed from
ACTIVE_DELEGATE_APIS.
Examples of DelegateAPIs can be found in adjutant.api.v1.openstack
Minimally they can look like this::
class NewCreateProject(BaseDelegateAPI):
@utils.authenticated
def post(self, request):
self.task_manager.create_from_request('my_custom_task', request)
return Response({'notes': ['task created']}, status=202)
Access can be restricted with the decorators mod_or_admin, project_admin and
admin decorators found in adjutant.api.utils. The request handlers are fairly
standard django view handlers and can execute any needed code. Additional
information for the task should be placed in request.data.
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
register_task_class(MyPluginTask)
Examples of tasks can be found in `adjutant.tasks.v1`
Minimally task should define their required fields::
class My(MyPluginTask):
task_type = "my_custom_task"
default_actions = [
"MyCustomAction",
]
duplicate_policy = "cancel" # default is cancel
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
register_action_class(NewClientSignUpAction, NewClientSignUpActionSerializer)
register_action_class(MyCustomAction, MyCustomActionSerializer)
Serializers can inherit from either rest_framework.serializers.Serializer, or
the current serializers in adjutant.actions.v1.serializers.
A task must both be registered with a valid URL and specified in
ACTIVE_TASKVIEWS in the configuration to be accessible.
A new task from a plugin can effectively 'override' a default task by
registering with the same URL, and sharing the task type. However it must have
a different class name and the previous task must be removed from
ACTIVE_TASKVIEWS.
**********************
Building Taskviews
**********************
Examples of taskviews can be found in adjutant.api.v1.openstack
Minimally they can look like this::
class NewCreateProject(TaskView):
task_type = "new_create_project"
default_actions = ["NewProjectWithUserAction", ]
def post(self, request, format=None):
processed, status = self.process_actions(request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with task." %
timezone.now())
return Response(errors, status=status)
return Response(response_dict, status=status)
Access can be restricted with the decorators mod_or_admin, project_admin and
admin decorators found in adjutant.api.utils. The request handlers are fairly
standard django view handlers and can execute any needed code. Additional
information for the actions should be placed in request.data.
*********************
Building Actions
*********************
Examples of actions can be found in adjutant.actions.v1.
Examples of actions can be found in `adjutant.actions.v1`
Minimally actions should define their required fields and implement 3
functions::
required = [
'user_id',
'value1',
]
class MyCustomAction(BaseAction):
def _pre_approve(self):
self.perform_action('initial')
required = [
'user_id',
'value1',
]
def _post_approve(self):
self.perform_action('token')
self.action.task.cache['value'] = self.value1
def _prepare(self):
# Do some validation here
pass
def _submit(self, data):
self.perform_action('completed')
self.add_note("Submit action performed")
def _approve(self):
# Do some logic here
self.action.task.cache['value'] = self.value1
def _submit(self, data):
# Do some logic here
self.add_note("Submit action performed")
Information set in the action task cache is available in email templates under
task.cache.value, and the action data is available in action.ActionName.value.
@ -127,7 +140,7 @@ Example::
from adjutant.actions.v1.serializers import BaseUserIdSerializer
from rest_framework import serializers
class NewActionSerializer(BaseUserIdSerializer):
class MyCustomActionSerializer(BaseUserIdSerializer):
value_1 = serializers.CharField()
******************************
@ -169,3 +182,6 @@ The Identity Manager is designed to replace access to the Keystone Client. It
can be imported from ``adjutant.actions.user_store.IdentityManager`` .
Functions for access to some of the other Openstack Clients are in
``adjutant.actions.openstack_clients``.
This will be expanded on in future, with the IdentityManager itself also
becoming pluggable.

View File

@ -0,0 +1,33 @@
---
features:
- |
Adjutant now introduces two new concepts for handling the configurable
APIs and workflow layer. DelegateAPIs are now the APIs which can be
customised and enabled in Adjutant, and Tasks are now their own layer
which can be called from the DelegateAPIs.
upgrade:
- |
* Major changes internal classes. Many plugins likely to need reworking
before using this release to match new internal changes.
* The Task database model has been renamed and moved, this will require
downtime for the migration to run, but should be fairly quick.
deprecations:
- |
* TaskViews are gone, and replaced with DelegateAPIs, with much of their old
logic now in the TaskManager and BaseTask.
* tasks config cannot override default_actions anymore
* standardized task API response codes on 202 unless task is completed from 200
* Action stages renamed to 'prepare', 'approve', 'submit'.
* TaskView logic and task defition moved to new task.v1 layer
* UserSetPassword API has been removed because it was a duplicate of
UserResetPassword.
* Removed redundant ip_address value on Task model
* multiple task_types have been renamed
* signup to create_project_and_user
* invite_user to invite_user_to_project
* reset_password to reset_user_password
* edit_user to edit_user_roles
* update_email to update_user_email
fixes:
- |
Reissuing task token now deletes old task tokens properly.

View File

@ -53,8 +53,8 @@ commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenote
ignore = D100,D101,D102,D103,D104,D105,D200,D203,D202,D204,D205,D208,D400,D401,W503
show-source = true
builtins = _
exclude=.venv,venv,.env,env,.git,.tox,dist,doc,*lib/python*,*egg,releasenotes,adjutant/api/migrations/*,adjutant/actions/migrations
exclude=.venv,venv,.env,env,.git,.tox,dist,doc,*lib/python*,*egg,releasenotes,adjutant/api/migrations/*,adjutant/actions/migrations,adjutant/tasks/migrations
[doc8]
ignore-path=.tox,*.egg-info,doc/build,.eggs/*/EGG-INFO/*.txt,./*.txt,adjutant
ignore-path=.tox,*.egg-info,doc/build,releasenotes/build,api-ref/build,.eggs/*/EGG-INFO/*.txt,./*.txt,adjutant
extension=.txt,.rst,.inc