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:
parent
d62eada126
commit
c9038dfe69
22
adjutant/actions/migrations/0004_auto_20190610_0209.py
Normal file
22
adjutant/actions/migrations/0004_auto_20190610_0209.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -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'])
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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})
|
||||
|
@ -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"])
|
||||
|
58
adjutant/api/exception_handler.py
Normal file
58
adjutant/api/exception_handler.py
Normal 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
|
23
adjutant/api/migrations/0005_auto_20190610_0209.py
Normal file
23
adjutant/api/migrations/0005_auto_20190610_0209.py
Normal 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',
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
22
adjutant/api/migrations/0006_auto_20190610_0209.py
Normal file
22
adjutant/api/migrations/0006_auto_20190610_0209.py
Normal 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'),
|
||||
),
|
||||
]
|
22
adjutant/api/migrations/0007_auto_20190610_0209.py
Normal file
22
adjutant/api/migrations/0007_auto_20190610_0209.py
Normal 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'),
|
||||
),
|
||||
]
|
26
adjutant/api/migrations/0008_auto_20190610_0209.py
Normal file
26
adjutant/api/migrations/0008_auto_20190610_0209.py
Normal 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',
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
@ -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
22
adjutant/api/v1/base.py
Normal 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
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
)
|
||||
|
@ -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):
|
||||
|
@ -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."]},
|
||||
|
@ -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:
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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.'
|
||||
|
@ -43,7 +43,7 @@ class EmailNotification(NotificationEngine):
|
||||
send an email with the given templates.
|
||||
|
||||
Example conf:
|
||||
<TaskView>:
|
||||
<task_type>:
|
||||
notifications:
|
||||
EmailNotification:
|
||||
standard:
|
||||
|
@ -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 }}
|
||||
|
@ -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",
|
||||
|
@ -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 = {}
|
||||
|
@ -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()
|
||||
|
0
adjutant/tasks/__init__.py
Normal file
0
adjutant/tasks/__init__.py
Normal file
44
adjutant/tasks/migrations/0001_initial.py
Normal file
44
adjutant/tasks/migrations/0001_initial.py
Normal 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': [],
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
79
adjutant/tasks/migrations/0002_auto_20190619_0613.py
Normal file
79
adjutant/tasks/migrations/0002_auto_20190619_0613.py
Normal 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'),
|
||||
),
|
||||
]
|
0
adjutant/tasks/migrations/__init__.py
Normal file
0
adjutant/tasks/migrations/__init__.py
Normal file
127
adjutant/tasks/models.py
Normal file
127
adjutant/tasks/models.py
Normal 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()
|
1
adjutant/tasks/v1/__init__.py
Normal file
1
adjutant/tasks/v1/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
default_app_config = 'adjutant.tasks.v1.app.TasksV1Config'
|
7
adjutant/tasks/v1/app.py
Normal file
7
adjutant/tasks/v1/app.py
Normal 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
438
adjutant/tasks/v1/base.py
Normal 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()
|
107
adjutant/tasks/v1/manager.py
Normal file
107
adjutant/tasks/v1/manager.py
Normal 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
|
43
adjutant/tasks/v1/models.py
Normal file
43
adjutant/tasks/v1/models.py
Normal 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)
|
24
adjutant/tasks/v1/projects.py
Normal file
24
adjutant/tasks/v1/projects.py
Normal 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",
|
||||
]
|
22
adjutant/tasks/v1/resources.py
Normal file
22
adjutant/tasks/v1/resources.py
Normal 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",
|
||||
]
|
48
adjutant/tasks/v1/users.py
Normal file
48
adjutant/tasks/v1/users.py
Normal 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
168
adjutant/tasks/v1/utils.py
Normal 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)
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
@ -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
|
||||
|
||||
|
||||
****************************
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
33
releasenotes/notes/story-2004489-857f37e4f6a0fe5c.yaml
Normal file
33
releasenotes/notes/story-2004489-857f37e4f6a0fe5c.yaml
Normal 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.
|
4
tox.ini
4
tox.ini
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user