notification changes

Mostly done, but does need proper tests.

* shift actions into their own sub folder structure for clarity
* create new sub folder for notications
* update other code to reflect those changes
* add first basic notification engine
* add RT notification engine
* minor django url and context changes to avoid future deprecation
* getting rid of secondary migration for column rename (as not in prod)

Change-Id: I46932b6d78b93e86580506c887548fd24c0750f5
This commit is contained in:
adriant 2015-11-03 15:10:55 +13:00
parent d4759d6734
commit 95e9eb4ba3
43 changed files with 393 additions and 147 deletions

View File

@ -1,3 +1,5 @@
include README.md
graft stacktask/api/v*/templates
graft stacktask/notifications/templates
graft stacktask/notifications/*/templates

View File

@ -8,7 +8,8 @@ ALLOWED_HOSTS:
ADDITIONAL_APPS:
- stacktask.api.v1
- stacktask.tenant_setup
- stacktask.actions.tenant_setup
- stacktask.notifications.request_tracker
DATABASES:
default:
@ -80,6 +81,19 @@ TASK_SETTINGS:
reply: no-reply@example.com
template: completed.txt
html_template: completed.txt
notifications:
EmailNotification:
emails:
- example@example.com
reply: no-reply@example.com
template: notification.txt
html_template: completed.txt
RTNotification:
url: http://localhost/rt/REST/1.0/
queue: helpdesk
username: example@example.com
password: password
template: notification.txt
invite_user:
emails:
# To not send this email, set the value to null,

View File

@ -6,3 +6,5 @@ python-keystoneclient>=1.0.0
python-neutronclient>=2.3.10
jsonfield>=1.0.2
django-rest-swagger>=0.3.3
pyyaml>=3.11
rt>=1.0.8

View File

@ -4,7 +4,7 @@ from setuptools import setup, find_packages
setup(
name='stacktask',
version='0.1.1a1',
version='0.1.1a2',
description='A user task service for openstack.',
long_description=(
'A task service to sit alongside keystone and ' +
@ -23,7 +23,11 @@ setup(
keywords='openstack keystone users tasks registration workflow',
packages=find_packages(),
package_data={'stacktask': ['api/v*/templates/*.txt']},
package_data={
'stacktask': [
'api/v*/templates/*.txt',
'notifications/templates/*.txt',
'notifications/*/templates/*.txt']},
install_requires=[
'Django>=1.7.3',
@ -34,7 +38,8 @@ setup(
'python-keystoneclient>=1.0.0',
'python-neutronclient>=2.3.10',
'pyyaml>=3.11',
'django-rest-swagger>=0.3.3'
'django-rest-swagger>=0.3.3',
'rt>=1.0.8',
],
entry_points={
'console_scripts': [

View File

@ -14,8 +14,8 @@
from django.db import models
from django.utils import timezone
from stacktask.base import user_store
from stacktask.base import serializers
from stacktask.actions import user_store
from stacktask.actions import serializers
from django.conf import settings
from jsonfield import JSONField
from logging import getLogger
@ -389,7 +389,11 @@ class NewProject(UserNameAction):
'email'
]
default_roles = {"Member", "project_owner", "project_mod", "_member_", "heat_stack_owner"}
# NOTE(adriant): move these to a config somewhere?
default_roles = {
"Member", "project_owner", "project_mod", "_member_",
"heat_stack_owner"
}
def _validate(self):
project_valid = self._validate_project()

View File

@ -1,4 +1,4 @@
from base.models import BaseAction
from actions.models import BaseAction
from serializers import NewClientSerializer
from django.conf import settings

View File

@ -12,11 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
from stacktask.base.models import BaseAction
from stacktask.tenant_setup.serializers import DefaultProjectResourcesSerializer
from stacktask.actions.models import BaseAction
from stacktask.actions.tenant_setup.serializers import DefaultProjectResourcesSerializer
from django.conf import settings
from stacktask.base.user_store import IdentityManager
from stacktask.base import openstack_clients
from stacktask.actions.user_store import IdentityManager
from stacktask.actions import openstack_clients
class DefaultProjectResources(BaseAction):

View File

@ -16,7 +16,8 @@ from django.test import TestCase
from stacktask.api.models import Task
from stacktask.api.v1.tests import FakeManager, setup_temp_cache
from stacktask.api.v1 import tests
from stacktask.tenant_setup.models import DefaultProjectResources, AddAdminToProject
from stacktask.actions.tenant_setup.models import (
DefaultProjectResources, AddAdminToProject)
import mock
@ -72,8 +73,9 @@ def get_fake_neutron():
class TenantSetupActionTests(TestCase):
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.openstack_clients.get_neutronclient',
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.openstack_clients.get_neutronclient',
get_fake_neutron)
def test_resource_setup(self):
"""
@ -110,8 +112,9 @@ class TenantSetupActionTests(TestCase):
self.assertEquals(len(neutron_cache['routers']), 1)
self.assertEquals(len(neutron_cache['subnets']), 1)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.openstack_clients.get_neutronclient',
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.openstack_clients.get_neutronclient',
get_fake_neutron)
def test_resource_setup_no_id(self):
"""
@ -141,8 +144,9 @@ class TenantSetupActionTests(TestCase):
self.assertEquals(len(neutron_cache['routers']), 0)
self.assertEquals(len(neutron_cache['subnets']), 0)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.openstack_clients.get_neutronclient',
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.openstack_clients.get_neutronclient',
get_fake_neutron)
def test_resource_setup_no_setup(self):
"""
@ -174,8 +178,9 @@ class TenantSetupActionTests(TestCase):
self.assertEquals(len(neutron_cache['routers']), 0)
self.assertEquals(len(neutron_cache['subnets']), 0)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.openstack_clients.get_neutronclient',
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.openstack_clients.get_neutronclient',
get_fake_neutron)
def test_resource_setup_fail(self):
"""
@ -231,7 +236,8 @@ class TenantSetupActionTests(TestCase):
self.assertEquals(len(neutron_cache['routers']), 1)
self.assertEquals(len(neutron_cache['subnets']), 1)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_add_admin(self):
"""
Base case, adds admin user with admin role to project.
@ -259,7 +265,8 @@ class TenantSetupActionTests(TestCase):
project = tests.temp_cache['projects']['test_project']
self.assertEquals(project.roles['admin'], ['admin'])
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_add_admin_reapprove(self):
"""
Ensure nothing happens or changes if rerun of approve.

View File

@ -16,16 +16,18 @@ from django.test import TestCase
from stacktask.api.models import Task
from stacktask.api.v1.tests import FakeManager, setup_temp_cache
from stacktask.api.v1 import tests
from stacktask.base.models import NewUser, NewProject, ResetUser, EditUserRoles
from stacktask.actions.models import (
NewUser, NewProject, ResetUser, EditUserRoles)
import mock
class BaseActionTests(TestCase):
class ActionTests(TestCase):
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_user(self):
"""
Test the base case, all valid.
Test the default case, all valid.
No existing user, valid tenant.
"""
project = mock.Mock()
@ -67,7 +69,8 @@ class BaseActionTests(TestCase):
self.assertEquals(project.roles['test@example.com'], ['Member'])
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_user_existing(self):
"""
Existing user, valid tenant, no role.
@ -85,9 +88,9 @@ class BaseActionTests(TestCase):
setup_temp_cache({'test_project': project}, {user.name: user})
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin',
'project_mod'],
'project_id': 'test_project_id'})
ip_address="0.0.0.0", keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id'})
data = {
'email': 'test@example.com',
@ -109,7 +112,8 @@ class BaseActionTests(TestCase):
self.assertEquals(project.roles[user.name], ['Member'])
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_user_existing_role(self):
"""
Existing user, valid tenant, has role.
@ -131,9 +135,9 @@ class BaseActionTests(TestCase):
setup_temp_cache({'test_project': project}, {user.name: user})
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin',
'project_mod'],
'project_id': 'test_project_id'})
ip_address="0.0.0.0", keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id'})
data = {
'email': 'test@example.com',
@ -156,7 +160,8 @@ class BaseActionTests(TestCase):
self.assertEquals(project.roles[user.name], ['Member'])
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_user_no_tenant(self):
"""
No user, no tenant.
@ -165,9 +170,9 @@ class BaseActionTests(TestCase):
setup_temp_cache({}, {})
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin',
'project_mod'],
'project_id': 'test_project_id'})
ip_address="0.0.0.0", keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id'})
data = {
'email': 'test@example.com',
@ -189,7 +194,8 @@ class BaseActionTests(TestCase):
self.assertEquals('admin' in tests.temp_cache['users'], True)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_project(self):
"""
Base case, no project, no user.
@ -201,9 +207,9 @@ class BaseActionTests(TestCase):
setup_temp_cache({}, {})
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin',
'project_mod'],
'project_id': 'test_project_id'})
ip_address="0.0.0.0", keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id'})
data = {
'email': 'test@example.com',
@ -233,9 +239,11 @@ class BaseActionTests(TestCase):
project = tests.temp_cache['projects']['test_project']
self.assertEquals(
sorted(project.roles['test@example.com']),
sorted(['Member', '_member_', 'project_owner', 'project_mod', 'heat_stack_owner']))
sorted(['Member', '_member_', 'project_owner',
'project_mod', 'heat_stack_owner']))
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_project_reapprove(self):
"""
Project created at post_approve step,
@ -245,9 +253,9 @@ class BaseActionTests(TestCase):
setup_temp_cache({}, {})
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin',
'project_mod'],
'project_id': 'test_project_id'})
ip_address="0.0.0.0", keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id'})
data = {
'email': 'test@example.com',
@ -285,9 +293,11 @@ class BaseActionTests(TestCase):
project = tests.temp_cache['projects']['test_project']
self.assertEquals(
sorted(project.roles['test@example.com']),
sorted(['Member', '_member_', 'project_owner', 'project_mod', 'heat_stack_owner']))
sorted(['Member', '_member_', 'project_owner',
'project_mod', 'heat_stack_owner']))
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_project_existing_user(self):
"""
no project, existing user.
@ -301,9 +311,9 @@ class BaseActionTests(TestCase):
setup_temp_cache({}, {user.name: user})
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin',
'project_mod'],
'project_id': 'test_project_id'})
ip_address="0.0.0.0", keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id'})
data = {
'email': 'test@example.com',
@ -332,9 +342,11 @@ class BaseActionTests(TestCase):
project = tests.temp_cache['projects']['test_project']
self.assertEquals(
sorted(project.roles['test@example.com']),
sorted(['Member', '_member_', 'project_owner', 'project_mod', 'heat_stack_owner']))
sorted(['Member', '_member_', 'project_owner',
'project_mod', 'heat_stack_owner']))
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_project_existing(self):
"""
Existing project.
@ -348,9 +360,9 @@ class BaseActionTests(TestCase):
setup_temp_cache({project.name: project}, {})
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin',
'project_mod'],
'project_id': 'test_project_id'})
ip_address="0.0.0.0", keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id'})
data = {
'email': 'test@example.com',
@ -365,7 +377,8 @@ class BaseActionTests(TestCase):
action.post_approve()
self.assertEquals(action.valid, False)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_reset_user(self):
"""
Base case, existing user.
@ -380,9 +393,9 @@ class BaseActionTests(TestCase):
setup_temp_cache({}, {user.name: user})
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin',
'project_mod'],
'project_id': 'test_project_id'})
ip_address="0.0.0.0", keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id'})
data = {
'email': 'test@example.com',
@ -405,7 +418,8 @@ class BaseActionTests(TestCase):
tests.temp_cache['users']['test@example.com'].password,
'123456')
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_reset_user_no_user(self):
"""
No user.
@ -414,9 +428,9 @@ class BaseActionTests(TestCase):
setup_temp_cache({}, {})
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin',
'project_mod'],
'project_id': 'test_project_id'})
ip_address="0.0.0.0", keystone_user={
'roles': ['admin', 'project_mod'],
'project_id': 'test_project_id'})
data = {
'email': 'test@example.com',
@ -435,7 +449,8 @@ class BaseActionTests(TestCase):
action.submit(token_data)
self.assertEquals(action.valid, False)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_edit_user_add(self):
"""
Add roles to existing user.
@ -480,7 +495,8 @@ class BaseActionTests(TestCase):
self.assertEquals(set(project.roles[user.name]),
set(['Member', 'project_mod']))
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_edit_user_add_complete(self):
"""
Add roles to existing user.
@ -526,7 +542,8 @@ class BaseActionTests(TestCase):
self.assertEquals(set(project.roles[user.name]),
set(['Member', 'project_mod']))
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_edit_user_remove(self):
"""
Remove roles from existing user.
@ -570,7 +587,8 @@ class BaseActionTests(TestCase):
self.assertEquals(project.roles[user.name], ['Member'])
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_edit_user_remove_complete(self):
"""
Remove roles from existing user.

View File

@ -18,6 +18,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('notes', jsonfield.fields.JSONField(default={})),
('error', models.BooleanField(default=False, db_index=True)),
('created_on', models.DateTimeField(default=django.utils.timezone.now)),
('acknowledged', models.BooleanField(default=False, db_index=True)),
],
@ -29,7 +30,7 @@ class Migration(migrations.Migration):
('ip_address', models.GenericIPAddressField()),
('keystone_user', jsonfield.fields.JSONField(default={})),
('project_id', models.CharField(max_length=200, null=True, db_index=True)),
('task_view', models.CharField(max_length=200, db_index=True)),
('task_type', models.CharField(max_length=200, db_index=True)),
('action_notes', jsonfield.fields.JSONField(default={})),
('cancelled', models.BooleanField(default=False, db_index=True)),
('approved', models.BooleanField(default=False, db_index=True)),

View File

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='task',
old_name='task_view',
new_name='task_type',
),
]

View File

@ -121,6 +121,7 @@ class Notification(models.Model):
notes = JSONField(default={})
task = models.ForeignKey(Task)
error = models.BooleanField(default=False, db_index=True)
created_on = models.DateTimeField(default=timezone.now)
acknowledged = models.BooleanField(default=False, db_index=True)

View File

@ -12,9 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import patterns, url, include
from django.conf.urls import url, include
urlpatterns = patterns(
'',
urlpatterns = [
url(r'^v1/', include('stacktask.api.v1.urls')),
)
]

View File

@ -11,6 +11,7 @@
# 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.utils import timezone
from django.conf import settings
from rest_framework.response import Response
@ -18,7 +19,7 @@ from rest_framework.response import Response
from stacktask.api.v1 import tasks
from stacktask.api import utils
from stacktask.api import models
from stacktask.base import user_store
from stacktask.actions import user_store
class UserList(tasks.InviteUser):

View File

@ -13,7 +13,7 @@
# under the License.
from rest_framework.response import Response
from stacktask.base.user_store import IdentityManager
from stacktask.actions.user_store import IdentityManager
from stacktask.api.models import Task
from django.utils import timezone
from stacktask.api import utils
@ -343,7 +343,8 @@ class InviteUser(TaskView):
self.logger.info("(%s) - New AttachUser request." % timezone.now())
# Default project_id to the keystone user's project
if 'project_id' not in request.data or request.data['project_id'] is None:
if ('project_id' not in request.data or
request.data['project_id'] is None):
request.data['project_id'] = request.keystone_user['project_id']
# TODO: First check if the user already exists or is pending

View File

@ -139,7 +139,8 @@ class APITests(APITestCase):
These tests also focus on authentication status
and role prermissions."""
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_user(self):
"""
Ensure the new user workflow goes as expected.
@ -173,7 +174,8 @@ class APITests(APITestCase):
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_user_no_project(self):
"""
Can't create a user for a non-existent project.
@ -195,7 +197,8 @@ class APITests(APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'errors': ['actions invalid']})
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_user_not_my_project(self):
"""
Can't create a user for project that isn't mine.
@ -216,7 +219,8 @@ class APITests(APITestCase):
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_new_user_not_authenticated(self):
"""
Can't create a user if unauthenticated.
@ -235,7 +239,8 @@ class APITests(APITestCase):
{'errors': ["Credentials incorrect or none given."]}
)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_add_user_existing(self):
"""
Adding existing user to project.
@ -273,7 +278,8 @@ class APITests(APITestCase):
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_add_user_existing_with_role(self):
"""
Adding existing user to project.
@ -309,8 +315,10 @@ class APITests(APITestCase):
response.data,
{'notes': 'Task completed successfully.'})
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_new_project(self):
"""
Ensure the new project workflow goes as expected.
@ -343,8 +351,10 @@ class APITests(APITestCase):
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_new_project_existing(self):
"""
Test to ensure validation marks actions as invalid
@ -387,8 +397,10 @@ class APITests(APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'errors': ['actions invalid']})
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_new_project_existing_user(self):
"""
Project created if not present, existing user attached.
@ -434,7 +446,8 @@ class APITests(APITestCase):
{'notes': 'Task completed successfully.'}
)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_reset_user(self):
"""
Ensure the reset user workflow goes as expected.
@ -462,7 +475,8 @@ class APITests(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(user.password, 'new_test_password')
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_reset_user_no_existing(self):
"""
Actions should be invalid.
@ -534,7 +548,8 @@ class APITests(APITestCase):
self.assertEqual(
response.data, {'errors': ['No task with this id.']})
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_token_expired_post(self):
"""
Expired token should do nothing, then delete itself.
@ -566,7 +581,8 @@ class APITests(APITestCase):
{'errors': ['This token does not exist or has expired.']})
self.assertEqual(0, Token.objects.count())
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_token_expired_get(self):
"""
Expired token should do nothing, then delete itself.
@ -597,8 +613,10 @@ class APITests(APITestCase):
{'errors': ['This token does not exist or has expired.']})
self.assertEqual(0, Token.objects.count())
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_task_complete(self):
"""
Can't approve a completed task.
@ -629,8 +647,10 @@ class APITests(APITestCase):
response.data,
{'errors': ['This task has already been completed.']})
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_task_update(self):
"""
Creates a invalid task.
@ -680,8 +700,10 @@ class APITests(APITestCase):
response.data,
{'notes': ['created token']})
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_notification_createproject(self):
"""
CreateProject should create a notification.
@ -712,8 +734,10 @@ class APITests(APITestCase):
response.data[0]['task'],
new_task.uuid)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_notification_acknowledge(self):
"""
Test that you can acknowledge a notification.
@ -753,8 +777,10 @@ class APITests(APITestCase):
response = self.client.get(url, headers=headers)
self.assertEqual(response.data, [])
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_notification_acknowledge_list(self):
"""
Test that you can acknowledge a list of notifications.
@ -792,7 +818,8 @@ class APITests(APITestCase):
response = self.client.get(url, headers=headers)
self.assertEqual(response.data, [])
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_token_expired_delete(self):
"""
test deleting of expired tokens.
@ -847,7 +874,8 @@ class APITests(APITestCase):
{'notes': ['Deleted all expired tokens.']})
self.assertEqual(Token.objects.count(), 1)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
def test_token_reissue(self):
"""
test for reissue of tokens
@ -891,8 +919,10 @@ class APITests(APITestCase):
new_token = Token.objects.all()[0]
self.assertNotEquals(new_token.token, uuid)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_cancel_task(self):
"""
Ensure the ability to cancel a task.
@ -927,8 +957,10 @@ class APITests(APITestCase):
headers=headers)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_cancel_task_sent_token(self):
"""
Ensure the ability to cancel a task after the token is sent.
@ -965,8 +997,10 @@ class APITests(APITestCase):
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@mock.patch('stacktask.base.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.tenant_setup.models.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_task_update_unapprove(self):
"""
Ensure task update doesn't work for approved actions.

View File

@ -12,14 +12,13 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import patterns, url
from django.conf.urls import url
from stacktask.api.v1 import views
from stacktask.api.v1 import tasks
from stacktask.api.v1 import openstack
urlpatterns = patterns(
'',
urlpatterns = [
url(r'^tasks/(?P<uuid>\w+)/?$', views.TaskDetail.as_view()),
url(r'^tasks/?$', views.TaskList.as_view()),
url(r'^tokens/(?P<id>\w+)', views.TokenDetail.as_view()),
@ -37,4 +36,4 @@ urlpatterns = patterns(
url(r'^openstack/users/(?P<user_id>\w+)/?$', openstack.UserDetail.as_view()),
url(r'^openstack/users/?$', openstack.UserList.as_view()),
url(r'^openstack/roles/?$', openstack.RoleList.as_view()),
)
]

View File

@ -5,7 +5,7 @@ from uuid import uuid4
from django.core.mail import send_mail
from smtplib import SMTPException
from django.conf import settings
from django.template import loader, Context
from django.template import loader
def create_token(task):
@ -61,8 +61,8 @@ def send_email(registration, email_conf, token=None):
context = {'registration': registration, 'actions': actions}
try:
message = template.render(Context(context))
html_message = html_template.render(Context(context))
message = template.render(context)
html_message = html_template.render(context)
send_mail(
email_conf['subject'], message, email_conf['reply'],
[emails.pop()], fail_silently=False, html_message=html_message)
@ -72,14 +72,22 @@ def send_email(registration, email_conf, token=None):
("Error: '%s' while emailing token for registration: %s" %
(e, registration.uuid))
}
create_notification(registration, notes)
create_notification(registration, notes, error=True)
# TODO(adriant): raise some error?
# and surround calls to this function with try/except
def create_notification(task, notes):
def create_notification(task, notes, error=False):
notification = Notification.objects.create(
task=task,
notes=notes
notes=notes,
error=error
)
notification.save()
class_conf = settings.TASK_SETTINGS[task.task_type]
# NOTE(adriant): some form of error handling is probably needed:
for note_engine, conf in class_conf.get('notifications', {}).iteritems():
engine = settings.NOTIFICATION_ENGINES[note_engine](conf)
engine.notify(task, notes, error)

View File

View File

@ -0,0 +1,77 @@
# 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.core.mail import send_mail
from django.template import loader
class NotificationEngine(object):
""""""
def __init__(self, conf):
self.conf = conf
def notify(self, task, notes, error):
return self._notify(task, notes, error)
def _notify(self, task, notes, error):
raise NotImplementedError
class EmailNotification(NotificationEngine):
"""
Basic email notification engine. Will
send an email in the given templates.
Example conf:
<TaskView>:
notifications:
EmailNotification:
emails:
- example@example.com
reply: no-reply@example.com
template: notification.txt
html_template: completed.txt
<other notification>:
...
"""
def notify(self, task, notes, error):
return self._notify(task, notes, error)
def _notify(self, task, notes, error):
template = loader.get_template(self.conf['template'])
html_template = loader.get_template(self.conf['html_template'])
context = {'task': task, 'notes': notes}
# NOTE(adriant): Error handling?
message = template.render(context)
html_message = html_template.render(context)
if error:
subject = "Error - %s notification" % task.task_type
else:
subject = "%s notification" % task.task_type
send_mail(
subject, message, self.conf['reply'],
self.conf['emails'], fail_silently=False,
html_message=html_message)
notification_engines = {
'EmailNotification': EmailNotification,
}
settings.NOTIFICATION_ENGINES.update(notification_engines)

View File

@ -0,0 +1,74 @@
# 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.template import loader
from stacktask.notifications.models import NotificationEngine
import rt
class RTNotification(NotificationEngine):
"""
Request Tracker notification engine. Will
create a new ticket in RT for the notification.
Example conf:
<TaskView>:
notifications:
RTNotification:
url: http://localhost/rt/REST/1.0/
queue: helpdesk
username: example@example.com
password: password
template: notification.txt
<other notification>:
...
"""
def __init__(self, conf):
super(RTNotification, self).__init__(conf)
# in memory dict to be used for passing data between actions:
tracker = rt.Rt(
self.conf['url'], self.conf['username'], self.conf['password'])
tracker.login()
self.tracker = tracker
def notify(self, task, notes, error):
return self._notify(task, notes, error)
def _notify(self, task, notes, error):
template = loader.get_template(self.conf['template'])
context = {'task': task, 'notes': notes}
# NOTE(adriant): Error handling?
message = template.render(context)
if error:
subject = "Error - %s notification" % task.task_type
else:
subject = "%s notification" % task.task_type
self.tracker.create_ticket(
Queue=self.conf['queue'], Subject=subject,
# newline + space tells the RT api to actually treat it like a
# newline.
Text=message.replace('\n', '\n '))
notification_engines = {
'RTNotification': RTNotification,
}
settings.NOTIFICATION_ENGINES.update(notification_engines)

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,10 @@
Hello,
task:
{{ task }}
notes:
{{ notes }}
Thank you for using our service.

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -46,8 +46,9 @@ INSTALLED_APPS = (
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_swagger',
'stacktask.base',
'stacktask.actions',
'stacktask.api',
'stacktask.notifications',
)
MIDDLEWARE_CLASSES = (
@ -152,3 +153,5 @@ ACTION_SETTINGS = CONFIG['ACTION_SETTINGS']
# Dict of actions and their serializers.
# - This is populated from the various model modules at startup:
ACTION_CLASSES = {}
NOTIFICATION_ENGINES = {}

View File

@ -16,7 +16,7 @@ SECRET_KEY = '+er!!4olta#17a=n%uotcazg2ncpl==yjog%1*o-(cr%zys-)!'
ADDITIONAL_APPS = [
'stacktask.api.v1',
'stacktask.tenant_setup'
'stacktask.actions.tenant_setup'
]
DATABASES = {

View File

@ -12,13 +12,12 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import patterns, include, url
from django.conf.urls import include, url
from django.conf import settings
urlpatterns = patterns(
'',
urlpatterns = [
url(r'^', include('stacktask.api.urls')),
)
]
if settings.DEBUG:
urlpatterns.append(url(r'^docs/', include('rest_framework_swagger.urls')))