Refinements for Signup

Reworking NewProject as a standalone and making NewProjectWithUser
for use with signup style tasks.

NewProject and NewProjectWithUser now create the project and user
at post_approve and then resets the user password at submit.
 - This change allows signup tokens to expire and a new signup to
   use the reset feature to still get access. The process still
   appears exactly the same to the end user.
 - Existing users creating a new project will also get created at
   post_approve step, but as they needed no token this
   functionality does not change from an outside perspective.

Fixing a project creation issue with keystone V3, wasn't setting
domain.

More standardisation in action handling functions.

Duplicate error now returns 409 rather than 400 for clarity.

Adding an "approved_by" values to tasks both for auditing and for
possible future logic checks.

Reworking of Network resource creation into two variant actions.

Reworking AddAdminToProject to be more generic and allow a list of
users.

Fixing issues with logic for task approval and task updating.

Change-Id: Ieba9907e5632dd441a86c41de291c6a7d0c8764a
This commit is contained in:
adrian-turjak 2016-08-12 14:53:27 +12:00 committed by Dale Smith
parent e5084a84ed
commit 57b54baabe
20 changed files with 1078 additions and 383 deletions

View File

@ -50,7 +50,7 @@ KEYSTONE:
project_name: admin project_name: admin
# MUST BE V3 API: # MUST BE V3 API:
auth_url: http://localhost:5000/v3 auth_url: http://localhost:5000/v3
DEFAULT_REGION: RegionOne domain_id: default
TOKEN_SUBMISSION_URL: http://192.168.122.160:8080/token/ TOKEN_SUBMISSION_URL: http://192.168.122.160:8080/token/
@ -129,8 +129,8 @@ TASK_SETTINGS:
# Additonal actions for views # Additonal actions for views
# These will run after the default actions, in the given order. # These will run after the default actions, in the given order.
additional_actions: additional_actions:
- AddAdminToProject - AddDefaultUsersToProject
- DefaultProjectResources - NewProjectDefaultNetwork
notifications: notifications:
standard: standard:
EmailNotification: EmailNotification:
@ -144,6 +144,10 @@ TASK_SETTINGS:
- signups@example.com - signups@example.com
RTNotification: RTNotification:
queue: signups queue: signups
default_region: RegionOne
# If 'None' (null in yaml), will default to domain as parent.
# If domain isn't set explicity, will service user domain (see KEYSTONE).
default_parent_id: null
invite_user: invite_user:
emails: emails:
# To not send this email, set the value to null # To not send this email, set the value to null
@ -187,6 +191,12 @@ TASK_SETTINGS:
# Action settings: # Action settings:
ACTION_SETTINGS: ACTION_SETTINGS:
NewProject:
default_roles:
- project_admin
- project_mod
- heat_stack_owner
- _member_
NewUser: NewUser:
allowed_roles: allowed_roles:
- project_admin - project_admin
@ -196,16 +206,21 @@ ACTION_SETTINGS:
ResetUser: ResetUser:
blacklisted_roles: blacklisted_roles:
- admin - admin
DefaultProjectResources: NewDefaultNetwork:
RegionOne: RegionOne:
network_name: somenetwork network_name: default_network
subnet_name: somesubnet subnet_name: default_subnet
router_name: somerouter router_name: default_router
public_network: 3cb50d61-5bce-4c03-96e6-8e262e12bb35 public_network: 3cb50d61-5bce-4c03-96e6-8e262e12bb35
DNS_NAMESERVERS: DNS_NAMESERVERS:
- 193.168.1.2 - 193.168.1.2
- 193.168.1.3 - 193.168.1.3
SUBNET_CIDR: 192.168.1.0/24 SUBNET_CIDR: 192.168.1.0/24
AddDefaultUsersToProject:
default_users:
- admin
default_roles:
- admin
# mapping between roles and managable roles # mapping between roles and managable roles
ROLES_MAPPING: ROLES_MAPPING:

View File

@ -13,6 +13,7 @@
# under the License. # under the License.
from logging import getLogger from logging import getLogger
from uuid import uuid4
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
@ -379,35 +380,22 @@ class NewUser(UserNameAction):
% (self.username, self.roles, self.project_id)) % (self.username, self.roles, self.project_id))
class NewProject(UserNameAction): class ProjectCreateBase(object):
""" """Mixin with functions for project creation."""
Similar functionality as the NewUser action,
but will create the project if valid. Will setup
the user (existing or new) with the 'default_role'.
"""
required = [ def _validate_parent_project(self):
'project_name', # NOTE(adriant): If parent id is None, Keystone defaults to the domain.
'username', # So we only care to validate if parent_id is not None.
'email' if self.parent_id:
] parent = self.id_manager.get_project(self.parent_id)
if not parent:
# NOTE(adriant): move these to a config somewhere? self.add_note("Parent id: '%s' does not exist." %
default_roles = { self.project_name)
"project_admin", "project_mod", "_member_", "heat_stack_owner" return False
} return True
def _validate(self):
project_valid = self._validate_project()
user_valid = self._validate_user()
self.action.valid = project_valid and user_valid
self.action.save()
def _validate_project(self): def _validate_project(self):
id_manager = user_store.IdentityManager() project = self.id_manager.find_project(self.project_name)
project = id_manager.find_project(self.project_name)
if project: if project:
self.add_note("Existing project with name '%s'." % self.add_note("Existing project with name '%s'." %
self.project_name) self.project_name)
@ -417,9 +405,148 @@ class NewProject(UserNameAction):
self.project_name) self.project_name)
return True return True
def _create_project(self):
try:
project = self.id_manager.create_project(
self.project_name, created_on=str(timezone.now()),
parent=self.parent_id)
except Exception as e:
self.add_note(
"Error: '%s' while creating project: %s" %
(e, self.project_name))
raise
# put project_id into action cache:
self.action.task.cache['project_id'] = project.id
self.set_cache('project_id', project.id)
self.add_note("New project '%s' created." % project.name)
def _grant_roles(self, user, roles, project_id):
ks_roles = []
for role in roles:
ks_role = self.id_manager.find_role(role)
if ks_role:
ks_roles.append(ks_role)
else:
raise TypeError("Keystone missing role: %s" % role)
for role in ks_roles:
self.id_manager.add_user_role(user, role, project_id)
# TODO(adriant): Write tests for this action.
class NewProject(BaseAction, ProjectCreateBase):
"""
Creates a new project for the current keystone_user.
This action can only be used for an autheticated taskview.
"""
required = [
'parent_id',
'project_name',
]
def __init__(self, *args, **kwargs):
super(NewProject, self).__init__(*args, **kwargs)
self.id_manager = user_store.IdentityManager()
def _validate(self):
valid_parent = self._validate_parent_project()
valid_project = self._validate_project()
self.action.valid = valid_project and valid_parent
self.action.save()
def _validate_parent_project(self):
if self.parent_id:
keystone_user = self.action.task.keystone_user
if self.parent_id != keystone_user['project_id']:
self.add_note(
'Parent id does not match keystone user project.')
return False
return super(NewProject, self)._validate_parent_project()
return True
def _pre_approve(self):
self._validate()
def _post_approve(self):
project_id = self.get_cache('project_id')
if project_id:
self.action.task.cache['project_id'] = project_id
self.add_note("Project already created.")
else:
self._validate()
if not self.valid:
return
self._create_project()
user_id = self.get_cache('user_id')
if user_id:
self.action.task.cache['user_id'] = user_id
self.add_note("User already given roles.")
else:
default_roles = settings.ACTION_SETTINGS.get(
'NewProject', {}).get("default_roles", {})
project_id = self.get_cache('project_id')
keystone_user = self.action.task.keystone_user
try:
user = self.id_manager.get_user(keystone_user['user_id'])
self._grant_roles(user, default_roles, project_id)
except Exception as e:
self.add_note(
("Error: '%s' while adding roles %s "
"to user '%s' on project '%s'") %
(e, self.username, default_roles, project_id))
raise
# put user_id into action cache:
self.action.task.cache['user_id'] = user.id
self.set_cache('user_id', user.id)
self.add_note(("Existing user '%s' attached to project %s" +
" with roles: %s")
% (self.username, project_id,
default_roles))
def _submit(self, token_data):
"""
Nothing to do here. Everything is done at post_approve.
"""
pass
class NewProjectWithUser(UserNameAction, ProjectCreateBase):
"""
Makes a new project for the given username. Will create the user if it
doesn't exists.
"""
required = [
'parent_id',
'project_name',
'username',
'email'
]
def __init__(self, *args, **kwargs):
super(NewProjectWithUser, self).__init__(*args, **kwargs)
self.id_manager = user_store.IdentityManager()
def _validate(self):
valid_parent = self._validate_parent_project()
project_valid = self._validate_project()
user_valid = self._validate_user()
self.action.valid = valid_parent and project_valid and user_valid
self.action.save()
def _validate_user(self): def _validate_user(self):
id_manager = user_store.IdentityManager() user = self.id_manager.find_user(self.username)
user = id_manager.find_user(self.username)
if user: if user:
if user.email == self.email: if user.email == self.email:
@ -441,104 +568,135 @@ class NewProject(UserNameAction):
return valid return valid
def _validate_user_submit(self):
user_id = self.get_cache('user_id')
project_id = self.get_cache('project_id')
user = self.id_manager.get_user(user_id)
project = self.id_manager.get_project(project_id)
if user and project:
self.action.valid = True
else:
self.action.valid = False
self.action.save()
def _pre_approve(self): def _pre_approve(self):
self._validate() self._validate()
def _post_approve(self): def _post_approve(self):
""" """
Approving a registration means we set up the project itself, Approving a new project means we set up the project itself,
and then the user registration token is valid for submission and and if the user doesn't exist, create it right away. An existing
creating the user themselves. user automatically gets added to the new project.
""" """
project_id = self.get_cache('project_id') project_id = self.get_cache('project_id')
if project_id: if project_id:
self.action.task.cache['project_id'] = project_id self.action.task.cache['project_id'] = project_id
self.add_note("Project already created.") self.add_note("Project already created.")
return else:
self.action.valid = (
self._validate_project() and self._validate_parent_project())
self.action.save()
self._validate() if not self.valid:
if not self.valid: return
return
id_manager = user_store.IdentityManager() self._create_project()
try:
project = id_manager.create_project( user_id = self.get_cache('user_id')
self.project_name, created_on=str(timezone.now())) if user_id:
except Exception as e: self.action.task.cache['user_id'] = user_id
self.add_note( self.add_note("User already created.")
"Error: '%s' while creating project: %s" % else:
(e, self.project_name)) self.action.valid = self._validate_user()
raise self.action.save()
# put project_id into action cache:
self.action.task.cache['project_id'] = project.id if not self.valid:
self.set_cache('project_id', project.id) return
self.add_note("New project '%s' created." % self.project_name)
default_roles = settings.ACTION_SETTINGS.get(
'NewProject', {}).get("default_roles", {})
project_id = self.get_cache('project_id')
if self.action.state == "default":
try:
# Generate a temporary password:
password = uuid4().hex + uuid4().hex
user = self.id_manager.create_user(
name=self.username, password=password,
email=self.email, project_id=project_id)
self._grant_roles(user, default_roles, project_id)
except Exception as e:
self.add_note(
"Error: '%s' while creating user: %s with roles: %s" %
(e, self.username, default_roles))
raise
# put user_id into action cache:
self.action.task.cache['user_id'] = user.id
self.set_cache('user_id', user.id)
self.add_note(
"New user '%s' created for project %s with roles: %s" %
(self.username, project_id, default_roles))
elif self.action.state == "existing":
try:
user = self.id_manager.find_user(self.username)
self._grant_roles(user, default_roles, project_id)
except Exception as e:
self.add_note(
"Error: '%s' while attaching user: %s with roles: %s" %
(e, self.username, default_roles))
raise
# put user_id into action cache:
self.action.task.cache['user_id'] = user.id
self.set_cache('user_id', user.id)
self.add_note(("Existing user '%s' attached to project %s" +
" with roles: %s")
% (self.username, project_id,
default_roles))
def _submit(self, token_data): def _submit(self, token_data):
""" """
The submit action is prformed when a token is submitted. The submit action is performed when a token is submitted.
This is done for a user account only, and so should now only This is done to set a user password only, and so should now only
set up the user, not the project, which was done in approve. change the user password. The project and user themselves are created
on post_approve.
""" """
id_manager = user_store.IdentityManager() self._validate_user_submit()
self.action.valid = self._validate_user()
self.action.save()
if not self.valid: if not self.valid:
return return
project_id = self.get_cache('project_id') project_id = self.get_cache('project_id')
self.action.task.cache['project_id'] = project_id self.action.task.cache['project_id'] = project_id
user_id = self.get_cache('user_id')
project = id_manager.get_project(project_id) self.action.task.cache['user_id'] = user_id
if self.action.state == "default": if self.action.state == "default":
user = self.id_manager.get_user(user_id)
try: try:
roles = [] self.id_manager.update_user_password(
for role in self.default_roles: user, token_data['password'])
ks_role = id_manager.find_role(role)
if ks_role:
roles.append(ks_role)
else:
raise TypeError("Keystone missing role: %s" % role)
user = id_manager.create_user(
name=self.username, password=token_data['password'],
email=self.email, project_id=project.id)
for role in roles:
id_manager.add_user_role(user, role, project.id)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while creating user: %s with roles: %s" % "Error: '%s' while changing password for user: %s" %
(e, self.username, self.default_roles)) (e, self.username))
raise raise
self.add_note('User %s password has been changed.' % self.username)
self.add_note(
"New user '%s' created for project %s with roles: %s" %
(self.username, self.project_name, self.default_roles))
elif self.action.state == "existing": elif self.action.state == "existing":
try: # do nothing, everything is already done.
user = id_manager.find_user(self.username) self.add_note(
"Existing user '%s' already attached to project %s" % (
roles = [] user_id, project_id))
for role in self.default_roles:
roles.append(id_manager.find_role(role))
for role in roles:
id_manager.add_user_role(user, role, project.id)
except Exception as e:
self.add_note(
"Error: '%s' while attaching user: %s with roles: %s" %
(e, self.username, self.default_roles))
raise
self.add_note(("Existing user '%s' attached to project %s" +
" with roles: %s")
% (self.username, self.project_name,
self.default_roles))
class ResetUser(UserNameAction): class ResetUser(UserNameAction):
@ -761,6 +919,7 @@ def register_action_class(action_class, serializer_class):
# Register each action model # Register each action model
register_action_class(NewUser, serializers.NewUserSerializer) register_action_class(NewUser, serializers.NewUserSerializer)
register_action_class(NewProject, serializers.NewProjectSerializer) register_action_class(
NewProjectWithUser, serializers.NewProjectWithUserSerializer)
register_action_class(ResetUser, serializers.ResetUserSerializer) register_action_class(ResetUser, serializers.ResetUserSerializer)
register_action_class(EditUserRoles, serializers.EditUserSerializer) register_action_class(EditUserRoles, serializers.EditUserSerializer)

View File

@ -29,21 +29,23 @@ def get_keystoneclient():
password=settings.KEYSTONE['password'], password=settings.KEYSTONE['password'],
project_name=settings.KEYSTONE['project_name'], project_name=settings.KEYSTONE['project_name'],
auth_url=settings.KEYSTONE['auth_url'], auth_url=settings.KEYSTONE['auth_url'],
user_domain_name="default", user_domain_id=settings.KEYSTONE.get('domain_id', "default"),
project_domain_name="default", project_domain_id=settings.KEYSTONE.get('domain_id', "default"),
) )
sess = session.Session(auth=auth) sess = session.Session(auth=auth)
auth = ks_client.Client(session=sess) auth = ks_client.Client(session=sess)
return auth return auth
def get_neutronclient(): def get_neutronclient(region):
# TODO(Adriant): Add region support. auth = v3.Password(
neutron = neutron_client.Client(
username=settings.KEYSTONE['username'], username=settings.KEYSTONE['username'],
password=settings.KEYSTONE['password'], password=settings.KEYSTONE['password'],
tenant_name=settings.KEYSTONE['project_name'], project_name=settings.KEYSTONE['project_name'],
auth_url=settings.KEYSTONE['auth_url'], auth_url=settings.KEYSTONE['auth_url'],
region_name=settings.DEFAULT_REGION user_domain_id=settings.KEYSTONE.get('domain_id', "default"),
project_domain_id=settings.KEYSTONE.get('domain_id', "default"),
) )
sess = session.Session(auth=auth)
neutron = neutron_client.Client(session=sess, region_name=region)
return neutron return neutron

View File

@ -44,7 +44,15 @@ class NewUserSerializer(BaseUserNameSerializer):
pass pass
class NewProjectSerializer(BaseUserNameSerializer): class NewProjectSerializer(serializers.Serializer):
parent_id = serializers.CharField(
max_length=200, default=None, allow_null=True)
project_name = serializers.CharField(max_length=200)
class NewProjectWithUserSerializer(BaseUserNameSerializer):
parent_id = serializers.CharField(
max_length=200, default=None, allow_null=True)
project_name = serializers.CharField(max_length=200) project_name = serializers.CharField(max_length=200)

View File

@ -13,13 +13,14 @@
# under the License. # under the License.
from stacktask.actions.models import BaseAction from stacktask.actions.models import BaseAction
from stacktask.actions.tenant_setup.serializers import DefaultProjectResourcesSerializer from stacktask.actions.tenant_setup.serializers import (
NewDefaultNetworkSerializer, NewProjectDefaultNetworkSerializer)
from django.conf import settings from django.conf import settings
from stacktask.actions.user_store import IdentityManager from stacktask.actions.user_store import IdentityManager
from stacktask.actions import openstack_clients from stacktask.actions import openstack_clients
class DefaultProjectResources(BaseAction): class NewDefaultNetwork(BaseAction):
""" """
This action will setup all required basic networking This action will setup all required basic networking
resources so that a new user can launch instances resources so that a new user can launch instances
@ -27,36 +28,62 @@ class DefaultProjectResources(BaseAction):
""" """
required = [ required = [
'setup_resources' 'setup_network',
'project_id',
'region',
] ]
region = settings.DEFAULT_REGION
defaults = settings.ACTION_SETTINGS['DefaultProjectResources'][region]
def _validate(self): def _validate(self):
project_id = self.action.task.cache.get('project_id', None) # Default state is invalid
self.action.valid = False
valid = False if not self.project_id:
if project_id:
valid = True
self.add_note('project_id given: %s' % project_id)
else:
self.add_note('No project_id given.') self.add_note('No project_id given.')
return valid return
def _setup_resources(self): if not self.region:
neutron = openstack_clients.get_neutronclient() self.add_note('No region given.')
return
project_id = self.action.task.cache['project_id'] keystone_user = self.action.task.keystone_user
if keystone_user.get('project_id') != self.project_id:
self.add_note('Project id does not match keystone user project.')
return
id_manager = IdentityManager()
project = id_manager.get_project(self.project_id)
if not project:
self.add_note('Project does not exist.')
return
self.add_note('Project_id: %s exists.' % project.id)
region = id_manager.find_region(self.region)
if not region:
self.add_note('Region does not exist.')
return
self.add_note('Region: %s exists.' % self.region)
self.defaults = settings.ACTION_SETTINGS.get(
'NewDefaultNetwork', {}).get(self.region, {})
if not self.defaults:
self.add_note('ERROR: No default settings for given region.')
return
self.action.valid = True
def _create_network(self):
neutron = openstack_clients.get_neutronclient(region=self.region)
if not self.get_cache('network_id'): if not self.get_cache('network_id'):
try: try:
network_body = { network_body = {
"network": { "network": {
"name": self.defaults['network_name'], "name": self.defaults['network_name'],
'tenant_id': project_id, 'tenant_id': self.project_id,
"admin_state_up": True "admin_state_up": True
} }
} }
@ -69,11 +96,11 @@ class DefaultProjectResources(BaseAction):
self.set_cache('network_id', network['network']['id']) self.set_cache('network_id', network['network']['id'])
self.add_note("Network %s created for project %s" % self.add_note("Network %s created for project %s" %
(self.defaults['network_name'], (self.defaults['network_name'],
self.action.task.cache['project_id'])) self.project_id))
else: else:
self.add_note("Network %s already created for project %s" % self.add_note("Network %s already created for project %s" %
(self.defaults['network_name'], (self.defaults['network_name'],
self.action.task.cache['project_id'])) self.project_id))
if not self.get_cache('subnet_id'): if not self.get_cache('subnet_id'):
try: try:
@ -81,7 +108,7 @@ class DefaultProjectResources(BaseAction):
"subnet": { "subnet": {
"network_id": self.get_cache('network_id'), "network_id": self.get_cache('network_id'),
"ip_version": 4, "ip_version": 4,
'tenant_id': project_id, 'tenant_id': self.project_id,
'dns_nameservers': self.defaults['DNS_NAMESERVERS'], 'dns_nameservers': self.defaults['DNS_NAMESERVERS'],
"cidr": self.defaults['SUBNET_CIDR'] "cidr": self.defaults['SUBNET_CIDR']
} }
@ -106,7 +133,7 @@ class DefaultProjectResources(BaseAction):
"external_gateway_info": { "external_gateway_info": {
"network_id": self.defaults['public_network'] "network_id": self.defaults['public_network']
}, },
'tenant_id': project_id, 'tenant_id': self.project_id,
"admin_state_up": True "admin_state_up": True
} }
} }
@ -118,10 +145,10 @@ class DefaultProjectResources(BaseAction):
raise raise
self.set_cache('router_id', router['router']['id']) self.set_cache('router_id', router['router']['id'])
self.add_note("Router created for project %s" % self.add_note("Router created for project %s" %
self.action.task.cache['project_id']) self.project_id)
else: else:
self.add_note("Router already created for project %s" % self.add_note("Router already created for project %s" %
self.action.task.cache['project_id']) self.project_id)
try: try:
interface_body = { interface_body = {
@ -136,76 +163,207 @@ class DefaultProjectResources(BaseAction):
self.add_note("Interface added to router for subnet") self.add_note("Interface added to router for subnet")
def _pre_approve(self): def _pre_approve(self):
# Not exactly valid, but not exactly invalid. self._validate()
self.action.valid = True
self.action.save() self.action.save()
def _post_approve(self): def _post_approve(self):
self.action.valid = self._validate() self._validate()
self.action.save() self.action.save()
if self.setup_resources and self.valid: if self.setup_network and self.valid:
self._setup_resources() self._create_network()
def _submit(self, token_data): def _submit(self, token_data):
pass pass
class AddAdminToProject(BaseAction): class NewProjectDefaultNetwork(NewDefaultNetwork):
""" """
Action to add 'admin' user to project for A variant of NewDefaultNetwork that expects the project
monitoring purposes. to not be created until after post_approve.
""" """
required = [
'setup_network',
'region',
]
def _pre_validate(self):
# Default state is invalid
self.action.valid = False
# We don't check project here as it doesn't exist yet.
if not self.region:
self.add_note('No region given.')
return
id_manager = IdentityManager()
region = id_manager.find_region(self.region)
if not region:
self.add_note('Region does not exist.')
return
self.add_note('Region: %s exists.' % self.region)
self.defaults = settings.ACTION_SETTINGS.get(
'NewDefaultNetwork', {}).get(self.region, {})
if not self.defaults:
self.add_note('ERROR: No default settings for given region.')
return
self.action.valid = True
def _validate(self): def _validate(self):
project_id = self.action.task.cache.get('project_id', None) # Default state is invalid
self.action.valid = False
valid = False self.project_id = self.action.task.cache.get('project_id', None)
if project_id:
valid = True if not self.project_id:
self.add_note('project_id given: %s' % project_id)
else:
self.add_note('No project_id given.') self.add_note('No project_id given.')
return valid return
if not self.region:
self.add_note('No region given.')
return
id_manager = IdentityManager()
project = id_manager.get_project(self.project_id)
if not project:
self.add_note('Project does not exist.')
return
self.add_note('Project_id: %s exists.' % project.id)
region = id_manager.find_region(self.region)
if not region:
self.add_note('Region does not exist.')
return
self.add_note('Region: %s exists.' % self.region)
self.defaults = settings.ACTION_SETTINGS.get(
'NewDefaultNetwork', {}).get(self.region, {})
if not self.defaults:
self.add_note('ERROR: No default settings for given region.')
return
self.action.valid = True
def _pre_approve(self): def _pre_approve(self):
# Not yet exactly valid, but not exactly invalid. self._pre_validate()
self.action.valid = True
self.action.save() self.action.save()
def _post_approve(self): def _post_approve(self):
self.action.valid = self._validate() self._validate()
self.action.save() self.action.save()
if self.setup_network and self.valid:
self._create_network()
class AddDefaultUsersToProject(BaseAction):
"""
The purpose of this action is to add a given set of users after
the creation of a new Project. This is mainly for administrative
purposes, and for users involved with migrations, monitoring, and
general admin tasks that should de present by default.
"""
def _validate_users(self):
self.users = settings.ACTION_SETTINGS.get(
'AddDefaultUsersToProject', {}).get('default_users', [])
self.roles = settings.ACTION_SETTINGS.get(
'AddDefaultUsersToProject', {}).get('default_roles', [])
id_manager = IdentityManager()
all_found = True
for user in self.users:
ks_user = id_manager.find_user(user)
if ks_user:
self.add_note('User: %s exists.' % user)
else:
self.add_note('ERROR: User: %s does not exist.' % user)
all_found = False
for role in self.roles:
ks_role = id_manager.find_role(role)
if ks_role:
self.add_note('Role: %s exists.' % role)
else:
self.add_note('ERROR: Role: %s does not exist.' % role)
all_found = False
if all_found:
return True
else:
return False
def _validate_project(self):
self.project_id = self.action.task.cache.get('project_id', None)
id_manager = IdentityManager()
project = id_manager.get_project(self.project_id)
if not project:
self.add_note('Project does not exist.')
return False
self.add_note('Project_id: %s exists.' % project.id)
return True
def _validate(self):
if self._validate_users() and self._validate_project():
self.action.valid = True
else:
self.action.valid = False
self.action.save()
def _pre_approve(self):
self.action.valid = self._validate_users()
self.action.save()
def _post_approve(self):
self._validate()
if self.valid and not self.action.state == "completed": if self.valid and not self.action.state == "completed":
id_manager = IdentityManager() id_manager = IdentityManager()
project = id_manager.get_project( project = id_manager.get_project(self.project_id)
self.action.task.cache['project_id'])
try: try:
user = id_manager.find_user(name="admin") for user in self.users:
role = id_manager.find_role(name="admin") ks_user = id_manager.find_user(name=user)
id_manager.add_user_role(user, role, project.id) for role in self.roles:
ks_role = id_manager.find_role(name=role)
id_manager.add_user_role(ks_user, ks_role, project.id)
self.add_note(
'User: "%s" given role: %s on project: %s.' %
(ks_user.name, ks_role.name, project.id))
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while adding admin to project: %s" % "Error: '%s' while adding users to project: %s" %
(e, project.id)) (e, project.id))
raise raise
self.action.state = "completed" self.action.state = "completed"
self.action.save() self.action.save()
self.add_note( self.add_note("All users added.")
'Admin has been added to %s.' %
self.action.task.cache['project_id'])
def _submit(self, token_data): def _submit(self, token_data):
pass pass
action_classes = { action_classes = {
'DefaultProjectResources': (DefaultProjectResources, 'NewDefaultNetwork': (
DefaultProjectResourcesSerializer), NewDefaultNetwork, NewDefaultNetworkSerializer),
'AddAdminToProject': (AddAdminToProject, None) 'NewProjectDefaultNetwork': (
NewProjectDefaultNetwork, NewProjectDefaultNetworkSerializer),
'AddDefaultUsersToProject': (AddDefaultUsersToProject, None)
} }

View File

@ -15,5 +15,12 @@
from rest_framework import serializers from rest_framework import serializers
class DefaultProjectResourcesSerializer(serializers.Serializer): class NewDefaultNetworkSerializer(serializers.Serializer):
setup_resources = serializers.BooleanField(default=False) setup_network = serializers.BooleanField(default=True)
project_id = serializers.CharField(max_length=100)
region = serializers.CharField(max_length=100)
class NewProjectDefaultNetworkSerializer(serializers.Serializer):
setup_network = serializers.BooleanField(default=False)
region = serializers.CharField(max_length=100)

View File

@ -17,7 +17,7 @@ from django.test import TestCase
import mock import mock
from stacktask.actions.tenant_setup.models import ( from stacktask.actions.tenant_setup.models import (
AddAdminToProject, DefaultProjectResources) NewDefaultNetwork, NewProjectDefaultNetwork, AddDefaultUsersToProject)
from stacktask.api.models import Task from stacktask.api.models import Task
from stacktask.api.v1 import tests from stacktask.api.v1 import tests
from stacktask.api.v1.tests import FakeManager, setup_temp_cache from stacktask.api.v1.tests import FakeManager, setup_temp_cache
@ -69,32 +69,44 @@ def setup_neutron_cache():
} }
def get_fake_neutron(): def get_fake_neutron(region):
return FakeNeutronClient() return FakeNeutronClient()
class TenantSetupActionTests(TestCase): class ProjectSetupActionTests(TestCase):
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager', @mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager) FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.openstack_clients.get_neutronclient', @mock.patch(
get_fake_neutron) 'stacktask.actions.tenant_setup.models.' +
def test_resource_setup(self): 'openstack_clients.get_neutronclient',
get_fake_neutron)
def test_network_setup(self):
""" """
Base case, setup resources, no issues. Base case, setup a new network , no issues.
""" """
setup_neutron_cache() setup_neutron_cache()
task = Task.objects.create( task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']}) ip_address="0.0.0.0",
keystone_user={
'roles': ['admin'],
'project_id': 'test_project_id'})
task.cache = {'project_id': "1"} project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.roles = {}
setup_temp_cache({'test_project': project}, {})
data = { data = {
'setup_resources': True, 'setup_network': True,
'region': 'RegionOne',
'project_id': 'test_project_id',
} }
action = DefaultProjectResources(data, task=task, action = NewDefaultNetwork(
order=1) data, task=task, order=1)
action.pre_approve() action.pre_approve()
self.assertEquals(action.valid, True) self.assertEquals(action.valid, True)
@ -116,56 +128,36 @@ class TenantSetupActionTests(TestCase):
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager', @mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager) FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.openstack_clients.get_neutronclient', @mock.patch(
get_fake_neutron) 'stacktask.actions.tenant_setup.models.' +
def test_resource_setup_no_id(self): 'openstack_clients.get_neutronclient',
""" get_fake_neutron)
No project id given, should do nothing. def test_network_setup_no_setup(self):
"""
setup_neutron_cache()
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
data = {
'setup_resources': True,
}
action = DefaultProjectResources(data, task=task,
order=1)
action.pre_approve()
self.assertEquals(action.valid, True)
action.post_approve()
self.assertEquals(action.valid, False)
self.assertEquals(action.action.cache, {})
global neutron_cache
self.assertEquals(len(neutron_cache['networks']), 0)
self.assertEquals(len(neutron_cache['routers']), 0)
self.assertEquals(len(neutron_cache['subnets']), 0)
@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):
""" """
Told not to setup, should do nothing. Told not to setup, should do nothing.
""" """
setup_neutron_cache() setup_neutron_cache()
task = Task.objects.create( task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']}) ip_address="0.0.0.0",
keystone_user={
'roles': ['admin'],
'project_id': 'test_project_id'})
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.roles = {}
setup_temp_cache({'test_project': project}, {})
data = { data = {
'setup_resources': False, 'setup_network': False,
'region': 'RegionOne',
'project_id': 'test_project_id',
} }
task.cache = {'project_id': "1"} action = NewDefaultNetwork(
data, task=task, order=1)
action = DefaultProjectResources(data, task=task,
order=1)
action.pre_approve() action.pre_approve()
self.assertEquals(action.valid, True) self.assertEquals(action.valid, True)
@ -182,25 +174,37 @@ class TenantSetupActionTests(TestCase):
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager', @mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager) FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.openstack_clients.get_neutronclient', @mock.patch(
get_fake_neutron) 'stacktask.actions.tenant_setup.models.' +
def test_resource_setup_fail(self): 'openstack_clients.get_neutronclient',
get_fake_neutron)
def test_network_setup_fail(self):
""" """
Should fail, but on re_approve will continue where it left off. Should fail, but on re_approve will continue where it left off.
""" """
setup_neutron_cache() setup_neutron_cache()
global neutron_cache global neutron_cache
task = Task.objects.create( task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']}) ip_address="0.0.0.0",
keystone_user={
'roles': ['admin'],
'project_id': 'test_project_id'})
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.roles = {}
setup_temp_cache({'test_project': project}, {})
data = { data = {
'setup_resources': True, 'setup_network': True,
'region': 'RegionOne',
'project_id': 'test_project_id',
} }
task.cache = {'project_id': "1"} action = NewDefaultNetwork(
data, task=task, order=1)
action = DefaultProjectResources(data, task=task,
order=1)
action.pre_approve() action.pre_approve()
self.assertEquals(action.valid, True) self.assertEquals(action.valid, True)
@ -240,9 +244,216 @@ class TenantSetupActionTests(TestCase):
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager', @mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager) FakeManager)
def test_add_admin(self): @mock.patch(
'stacktask.actions.tenant_setup.models.' +
'openstack_clients.get_neutronclient',
get_fake_neutron)
def test_new_project_network_setup(self):
"""
Base case, setup network after a new project, no issues.
"""
setup_neutron_cache()
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
data = {
'setup_network': True,
'region': 'RegionOne',
}
action = NewProjectDefaultNetwork(
data, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, True)
# Now we add the project data as this is where the project
# would be created:
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.roles = {}
setup_temp_cache({'test_project': project}, {})
task.cache = {'project_id': "test_project_id"}
action.post_approve()
self.assertEquals(action.valid, True)
self.assertEquals(
action.action.cache,
{'network_id': 'net_id_0',
'router_id': 'router_id_2',
'subnet_id': 'subnet_id_1'}
)
global neutron_cache
self.assertEquals(len(neutron_cache['networks']), 1)
self.assertEquals(len(neutron_cache['routers']), 1)
self.assertEquals(len(neutron_cache['subnets']), 1)
@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_new_project_network_setup_no_id(self):
"""
No project id given, should do nothing.
"""
setup_neutron_cache()
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
data = {
'setup_network': True,
'region': 'RegionOne',
}
action = NewProjectDefaultNetwork(
data, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, True)
action.post_approve()
self.assertEquals(action.valid, False)
self.assertEquals(action.action.cache, {})
global neutron_cache
self.assertEquals(len(neutron_cache['networks']), 0)
self.assertEquals(len(neutron_cache['routers']), 0)
self.assertEquals(len(neutron_cache['subnets']), 0)
@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_new_project_network_setup_no_setup(self):
"""
Told not to setup, should do nothing.
"""
setup_neutron_cache()
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
data = {
'setup_network': False,
'region': 'RegionOne',
}
action = NewProjectDefaultNetwork(
data, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, True)
# Now we add the project data as this is where the project
# would be created:
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.roles = {}
setup_temp_cache({'test_project': project}, {})
task.cache = {'project_id': "test_project_id"}
action.post_approve()
self.assertEquals(action.valid, True)
self.assertEquals(action.action.cache, {})
global neutron_cache
self.assertEquals(len(neutron_cache['networks']), 0)
self.assertEquals(len(neutron_cache['routers']), 0)
self.assertEquals(len(neutron_cache['subnets']), 0)
@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_new_project_network_setup_fail(self):
"""
Should fail, but on re_approve will continue where it left off.
"""
setup_neutron_cache()
global neutron_cache
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
data = {
'setup_network': True,
'region': 'RegionOne',
}
action = NewProjectDefaultNetwork(
data, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, True)
neutron_cache['routers'] = []
# Now we add the project data as this is where the project
# would be created:
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.roles = {}
setup_temp_cache({'test_project': project}, {})
task.cache = {'project_id': "test_project_id"}
try:
action.post_approve()
self.fail("Shouldn't get here.")
except Exception:
pass
self.assertEquals(
action.action.cache,
{'network_id': 'net_id_0',
'subnet_id': 'subnet_id_1'}
)
self.assertEquals(len(neutron_cache['networks']), 1)
self.assertEquals(len(neutron_cache['subnets']), 1)
self.assertEquals(len(neutron_cache['routers']), 0)
neutron_cache['routers'] = {}
action.post_approve()
self.assertEquals(
action.action.cache,
{'network_id': 'net_id_0',
'router_id': 'router_id_2',
'subnet_id': 'subnet_id_1'}
)
self.assertEquals(len(neutron_cache['networks']), 1)
self.assertEquals(len(neutron_cache['routers']), 1)
self.assertEquals(len(neutron_cache['subnets']), 1)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_add_default_users(self):
""" """
Base case, adds admin user with admin role to project. Base case, adds admin user with admin role to project.
NOTE(adriant): both the lists of users, and the roles to add
come from test_settings. This test assumes the conf setting of:
default_users = ['admin']
default_roles = ['admin']
""" """
project = mock.Mock() project = mock.Mock()
project.id = 'test_project_id' project.id = 'test_project_id'
@ -256,7 +467,7 @@ class TenantSetupActionTests(TestCase):
task.cache = {'project_id': "test_project_id"} task.cache = {'project_id': "test_project_id"}
action = AddAdminToProject({}, task=task, order=1) action = AddDefaultUsersToProject({}, task=task, order=1)
action.pre_approve() action.pre_approve()
self.assertEquals(action.valid, True) self.assertEquals(action.valid, True)
@ -285,7 +496,7 @@ class TenantSetupActionTests(TestCase):
task.cache = {'project_id': "test_project_id"} task.cache = {'project_id': "test_project_id"}
action = AddAdminToProject({}, task=task, order=1) action = AddDefaultUsersToProject({}, task=task, order=1)
action.pre_approve() action.pre_approve()
self.assertEquals(action.valid, True) self.assertEquals(action.valid, True)

View File

@ -17,7 +17,7 @@ from django.test import TestCase
import mock import mock
from stacktask.actions.models import ( from stacktask.actions.models import (
EditUserRoles, NewProject, NewUser, ResetUser) EditUserRoles, NewProjectWithUser, NewUser, ResetUser)
from stacktask.api.models import Task from stacktask.api.models import Task
from stacktask.api.v1 import tests from stacktask.api.v1 import tests
from stacktask.api.v1.tests import FakeManager, setup_temp_cache from stacktask.api.v1.tests import FakeManager, setup_temp_cache
@ -201,8 +201,8 @@ class ActionTests(TestCase):
""" """
Base case, no project, no user. Base case, no project, no user.
Project created at post_approve step, Project and user created at post_approve step,
user at submit step. user password at submit step.
""" """
setup_temp_cache({}, {}) setup_temp_cache({}, {})
@ -213,11 +213,12 @@ class ActionTests(TestCase):
'project_id': 'test_project_id'}) 'project_id': 'test_project_id'})
data = { data = {
'parent_id': None,
'email': 'test@example.com', 'email': 'test@example.com',
'project_name': 'test_project', 'project_name': 'test_project',
} }
action = NewProject(data, task=task, order=1) action = NewProjectWithUser(data, task=task, order=1)
action.pre_approve() action.pre_approve()
self.assertEquals(action.valid, True) self.assertEquals(action.valid, True)
@ -227,7 +228,9 @@ class ActionTests(TestCase):
self.assertEquals( self.assertEquals(
tests.temp_cache['projects']['test_project'].name, tests.temp_cache['projects']['test_project'].name,
'test_project') 'test_project')
self.assertEquals(task.cache, {'project_id': "project_id_1"}) self.assertEquals(
task.cache,
{'project_id': 'project_id_1', 'user_id': 'user_id_1'})
token_data = {'password': '123456'} token_data = {'password': '123456'}
action.submit(token_data) action.submit(token_data)
@ -257,11 +260,12 @@ class ActionTests(TestCase):
'project_id': 'test_project_id'}) 'project_id': 'test_project_id'})
data = { data = {
'parent_id': None,
'email': 'test@example.com', 'email': 'test@example.com',
'project_name': 'test_project', 'project_name': 'test_project',
} }
action = NewProject(data, task=task, order=1) action = NewProjectWithUser(data, task=task, order=1)
action.pre_approve() action.pre_approve()
self.assertEquals(action.valid, True) self.assertEquals(action.valid, True)
@ -271,14 +275,18 @@ class ActionTests(TestCase):
self.assertEquals( self.assertEquals(
tests.temp_cache['projects']['test_project'].name, tests.temp_cache['projects']['test_project'].name,
'test_project') 'test_project')
self.assertEquals(task.cache, {'project_id': "project_id_1"}) self.assertEquals(
task.cache,
{'project_id': 'project_id_1', 'user_id': 'user_id_1'})
action.post_approve() action.post_approve()
self.assertEquals(action.valid, True) self.assertEquals(action.valid, True)
self.assertEquals( self.assertEquals(
tests.temp_cache['projects']['test_project'].name, tests.temp_cache['projects']['test_project'].name,
'test_project') 'test_project')
self.assertEquals(task.cache, {'project_id': "project_id_1"}) self.assertEquals(
task.cache,
{'project_id': 'project_id_1', 'user_id': 'user_id_1'})
token_data = {'password': '123456'} token_data = {'password': '123456'}
action.submit(token_data) action.submit(token_data)
@ -301,7 +309,7 @@ class ActionTests(TestCase):
""" """
user = mock.Mock() user = mock.Mock()
user.id = 'user_id' user.id = 'user_id_1'
user.name = "test@example.com" user.name = "test@example.com"
user.email = "test@example.com" user.email = "test@example.com"
@ -313,11 +321,12 @@ class ActionTests(TestCase):
'project_id': 'test_project_id'}) 'project_id': 'test_project_id'})
data = { data = {
'parent_id': None,
'email': 'test@example.com', 'email': 'test@example.com',
'project_name': 'test_project', 'project_name': 'test_project',
} }
action = NewProject(data, task=task, order=1) action = NewProjectWithUser(data, task=task, order=1)
action.pre_approve() action.pre_approve()
self.assertEquals(action.valid, True) self.assertEquals(action.valid, True)
@ -327,7 +336,9 @@ class ActionTests(TestCase):
self.assertEquals( self.assertEquals(
tests.temp_cache['projects']['test_project'].name, tests.temp_cache['projects']['test_project'].name,
'test_project') 'test_project')
self.assertEquals(task.cache, {'project_id': "project_id_1"}) self.assertEquals(
task.cache,
{'project_id': 'project_id_1', 'user_id': 'user_id_1'})
token_data = {'password': '123456'} token_data = {'password': '123456'}
action.submit(token_data) action.submit(token_data)
@ -362,11 +373,12 @@ class ActionTests(TestCase):
'project_id': 'test_project_id'}) 'project_id': 'test_project_id'})
data = { data = {
'parent_id': None,
'email': 'test@example.com', 'email': 'test@example.com',
'project_name': 'test_project', 'project_name': 'test_project',
} }
action = NewProject(data, task=task, order=1) action = NewProjectWithUser(data, task=task, order=1)
action.pre_approve() action.pre_approve()
self.assertEquals(action.valid, False) self.assertEquals(action.valid, False)

View File

@ -152,7 +152,32 @@ class IdentityManager(object):
project = None project = None
return project return project
def create_project(self, project_name, created_on): def update_project(self, project, name=None, domain=None, description=None,
project = self.ks_client.projects.create(project_name, enabled=None, **kwargs):
created_on=created_on) try:
return self.ks_client.projects.update(
project=project, domain=domain, name=name,
description=description, enabled=enabled,
**kwargs)
except ks_exceptions.NotFound:
return None
def create_project(
self, project_name, created_on, parent=None, domain="default"):
project = self.ks_client.projects.create(
project_name, domain, parent=parent, created_on=created_on)
return project return project
def find_region(self, region_name):
try:
region = self.ks_client.regions.find(name=region_name)
except ks_exceptions.NotFound:
region = None
return region
def get_region(self, region_id):
try:
region = self.ks_client.regions.get(region_id)
except ks_exceptions.NotFound:
region = None
return region

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import jsonfield.fields
class Migration(migrations.Migration):
dependencies = [
('api', '0002_auto_20160815_2249'),
]
operations = [
migrations.AddField(
model_name='task',
name='approved_by',
field=jsonfield.fields.JSONField(default={}),
),
]

View File

@ -37,6 +37,9 @@ class Task(models.Model):
keystone_user = JSONField(default={}) keystone_user = JSONField(default={})
project_id = models.CharField(max_length=32, db_index=True, null=True) project_id = models.CharField(max_length=32, db_index=True, null=True)
# keystone_user for the approver:
approved_by = JSONField(default={})
# type of the task, for easy grouping # type of the task, for easy grouping
task_type = models.CharField(max_length=100, db_index=True) task_type = models.CharField(max_length=100, db_index=True)
@ -81,6 +84,7 @@ class Task(models.Model):
"uuid": self.uuid, "uuid": self.uuid,
"ip_address": self.ip_address, "ip_address": self.ip_address,
"keystone_user": self.keystone_user, "keystone_user": self.keystone_user,
"approved_by": self.approved_by,
"project_id": self.project_id, "project_id": self.project_id,
"actions": actions, "actions": actions,
"task_type": self.task_type, "task_type": self.task_type,

View File

@ -194,18 +194,18 @@ class UserRoles(tasks.TaskView):
request.data['user_id'] = user_id request.data['user_id'] = user_id
self.logger.info("(%s) - New EditUserRoles request." % timezone.now()) self.logger.info("(%s) - New EditUserRoles request." % timezone.now())
processed = self.process_actions(request) processed, status = self.process_actions(request)
errors = processed.get('errors', None) errors = processed.get('errors', None)
if errors: if errors:
self.logger.info("(%s) - Validation errors with registration." % self.logger.info("(%s) - Validation errors with registration." %
timezone.now()) timezone.now())
return Response(errors, status=400) return Response(errors, status=status)
task = processed['task'] task = processed['task']
self.logger.info("(%s) - AutoApproving EditUserRoles request." self.logger.info("(%s) - AutoApproving EditUserRoles request."
% timezone.now()) % timezone.now())
response_dict, status = self.approve(task) response_dict, status = self.approve(request, task)
add_task_id_for_roles(request, processed, response_dict, ['admin']) add_task_id_for_roles(request, processed, response_dict, ['admin'])
@ -223,18 +223,18 @@ class UserRoles(tasks.TaskView):
request.data['user_id'] = user_id request.data['user_id'] = user_id
self.logger.info("(%s) - New EditUser request." % timezone.now()) self.logger.info("(%s) - New EditUser request." % timezone.now())
processed = self.process_actions(request) processed, status = self.process_actions(request)
errors = processed.get('errors', None) errors = processed.get('errors', None)
if errors: if errors:
self.logger.info("(%s) - Validation errors with registration." % self.logger.info("(%s) - Validation errors with registration." %
timezone.now()) timezone.now())
return Response(errors, status=400) return Response(errors, status=status)
task = processed['task'] task = processed['task']
self.logger.info("(%s) - AutoApproving EditUser request." self.logger.info("(%s) - AutoApproving EditUser request."
% timezone.now()) % timezone.now())
response_dict, status = self.approve(task) response_dict, status = self.approve(request, task)
add_task_id_for_roles(request, processed, response_dict, ['admin']) add_task_id_for_roles(request, processed, response_dict, ['admin'])

View File

@ -110,7 +110,7 @@ class TaskView(APIViewWithLogger):
for action in action_list: for action in action_list:
if action['serializer'] is not None: if action['serializer'] is not None:
errors.update(action['serializer'].errors) errors.update(action['serializer'].errors)
return {'errors': errors} return {'errors': errors}, 400
hash_key = create_task_hash(self.task_type, action_list) hash_key = create_task_hash(self.task_type, action_list)
duplicate_tasks = Task.objects.filter( duplicate_tasks = Task.objects.filter(
@ -131,7 +131,9 @@ class TaskView(APIViewWithLogger):
self.logger.info( self.logger.info(
"(%s) - Task is a duplicate - Ignoring new task." % "(%s) - Task is a duplicate - Ignoring new task." %
timezone.now()) timezone.now())
return {'errors': ['Task is a duplicate of an existing task']} return (
{'errors': ['Task is a duplicate of an existing task']},
409)
ip_address = request.META['REMOTE_ADDR'] ip_address = request.META['REMOTE_ADDR']
keystone_user = request.keystone_user keystone_user = request.keystone_user
@ -182,22 +184,27 @@ class TaskView(APIViewWithLogger):
["Error: Something went wrong on the server. " + ["Error: Something went wrong on the server. " +
"It will be looked into shortly."] "It will be looked into shortly."]
} }
return response_dict return response_dict, 200
# send initial conformation email: # send initial conformation email:
email_conf = class_conf.get('emails', {}).get('initial', None) email_conf = class_conf.get('emails', {}).get('initial', None)
send_email(task, email_conf) send_email(task, email_conf)
return {'task': task} return {'task': task}, 200
def approve(self, task): def approve(self, request, task):
""" """
Approves the task and runs the post_approve steps. Approves the task and runs the post_approve steps.
Will create a token if required, otherwise will run the Will create a token if required, otherwise will run the
submit steps. submit steps.
""" """
# 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 = True
task.approved_on = timezone.now() task.approved_on = timezone.now()
task.approved_by = request.keystone_user
task.save() task.save()
action_models = task.actions action_models = task.actions
@ -320,11 +327,11 @@ class CreateProject(TaskView):
task_type = "create_project" task_type = "create_project"
default_actions = ["NewProject", ] default_actions = ["NewProjectWithUser", ]
def post(self, request, format=None): def post(self, request, format=None):
""" """
Unauthenticated endpoint bound primarily to NewProject. Unauthenticated endpoint bound primarily to NewProjectWithUser.
This process requires approval, so this will validate This process requires approval, so this will validate
incoming data and create a task to be approved incoming data and create a task to be approved
@ -332,7 +339,16 @@ class CreateProject(TaskView):
""" """
self.logger.info("(%s) - Starting new project task." % self.logger.info("(%s) - Starting new project task." %
timezone.now()) timezone.now())
processed = self.process_actions(request)
class_conf = settings.TASK_SETTINGS.get(self.task_type, {})
# we need to set the region the resources will be created in:
request.data['region'] = class_conf.get('default_region')
# 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)
errors = processed.get('errors', None) errors = processed.get('errors', None)
if errors: if errors:
@ -351,7 +367,7 @@ class CreateProject(TaskView):
add_task_id_for_roles(request, processed, response_dict, ['admin']) add_task_id_for_roles(request, processed, response_dict, ['admin'])
return Response(response_dict, status=200) return Response(response_dict, status=status)
class InviteUser(TaskView): class InviteUser(TaskView):
@ -383,19 +399,19 @@ class InviteUser(TaskView):
# TODO: First check if the user already exists or is pending # TODO: First check if the user already exists or is pending
# We should not allow duplicate invites. # We should not allow duplicate invites.
processed = self.process_actions(request) processed, status = self.process_actions(request)
errors = processed.get('errors', None) errors = processed.get('errors', None)
if errors: if errors:
self.logger.info("(%s) - Validation errors with task." % self.logger.info("(%s) - Validation errors with task." %
timezone.now()) timezone.now())
return Response(errors, status=400) return Response(errors, status=status)
task = processed['task'] task = processed['task']
self.logger.info("(%s) - AutoApproving AttachUser request." self.logger.info("(%s) - AutoApproving AttachUser request."
% timezone.now()) % timezone.now())
response_dict, status = self.approve(task) response_dict, status = self.approve(request, task)
add_task_id_for_roles(request, processed, response_dict, ['admin']) add_task_id_for_roles(request, processed, response_dict, ['admin'])
@ -432,19 +448,19 @@ class ResetPassword(TaskView):
""" """
self.logger.info("(%s) - New ResetUser request." % timezone.now()) self.logger.info("(%s) - New ResetUser request." % timezone.now())
processed = self.process_actions(request) processed, status = self.process_actions(request)
errors = processed.get('errors', None) errors = processed.get('errors', None)
if errors: if errors:
self.logger.info("(%s) - Validation errors with task." % self.logger.info("(%s) - Validation errors with task." %
timezone.now()) timezone.now())
return Response(errors, status=400) return Response(errors, status=status)
task = processed['task'] task = processed['task']
self.logger.info("(%s) - AutoApproving Resetuser request." self.logger.info("(%s) - AutoApproving Resetuser request."
% timezone.now()) % timezone.now())
self.approve(task) self.approve(request, task)
response_dict = {'notes': [ response_dict = {'notes': [
"If user with email exists, reset token will be issued."]} "If user with email exists, reset token will be issued."]}
@ -513,18 +529,18 @@ class EditUser(TaskView):
post_approve validation, and creates a Token if valid. post_approve validation, and creates a Token if valid.
""" """
self.logger.info("(%s) - New EditUser request." % timezone.now()) self.logger.info("(%s) - New EditUser request." % timezone.now())
processed = self.process_actions(request) processed, status = self.process_actions(request)
errors = processed.get('errors', None) errors = processed.get('errors', None)
if errors: if errors:
self.logger.info("(%s) - Validation errors with task." % self.logger.info("(%s) - Validation errors with task." %
timezone.now()) timezone.now())
return Response(errors, status=400) return Response(errors, status=status)
task = processed['task'] task = processed['task']
self.logger.info("(%s) - AutoApproving EditUser request." self.logger.info("(%s) - AutoApproving EditUser request."
% timezone.now()) % timezone.now())
response_dict, status = self.approve(task) response_dict, status = self.approve(request, task)
add_task_id_for_roles(request, processed, response_dict, ['admin']) add_task_id_for_roles(request, processed, response_dict, ['admin'])

View File

@ -27,6 +27,10 @@ def setup_temp_cache(projects, users):
users.update({admin_user.id: admin_user}) users.update({admin_user.id: admin_user})
region_one = mock.Mock()
region_one.id = 'region_id_0'
region_one.name = 'RegionOne'
global temp_cache global temp_cache
temp_cache = { temp_cache = {
@ -39,6 +43,9 @@ def setup_temp_cache(projects, users):
'project_admin': 'project_admin', 'project_admin': 'project_admin',
'project_mod': 'project_mod', 'project_mod': 'project_mod',
'heat_stack_owner': 'heat_stack_owner' 'heat_stack_owner': 'heat_stack_owner'
},
'regions': {
'RegionOne': region_one,
} }
} }
@ -165,7 +172,7 @@ class FakeManager(object):
if project.id == project_id: if project.id == project_id:
return FakeProject(project) return FakeProject(project)
def create_project(self, project_name, created_on, p_id=None): def create_project(self, project_name, created_on, parent=None, p_id=None):
global temp_cache global temp_cache
project = mock.Mock() project = mock.Mock()
if p_id: if p_id:
@ -174,6 +181,18 @@ class FakeManager(object):
temp_cache['i'] += 0.5 temp_cache['i'] += 0.5
project.id = "project_id_%s" % int(temp_cache['i']) project.id = "project_id_%s" % int(temp_cache['i'])
project.name = project_name project.name = project_name
# TODO(adriant): Do something better with the parent value.
project.parent = parent
project.roles = {} project.roles = {}
temp_cache['projects'][project_name] = project temp_cache['projects'][project_name] = project
return project return project
def find_region(self, region_name):
global temp_cache
return temp_cache['regions'].get(region_name, None)
def get_region(self, region_id):
global temp_cache
for region in temp_cache['regions'].values():
if region.id == region_id:
return region

View File

@ -59,6 +59,8 @@ class AdminAPITests(APITestCase):
@mock.patch( @mock.patch(
'stacktask.actions.models.user_store.IdentityManager', FakeManager) 'stacktask.actions.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_task_get(self): def test_task_get(self):
""" """
Test the basic task detail view. Test the basic task detail view.
@ -260,7 +262,11 @@ class AdminAPITests(APITestCase):
headers=headers) headers=headers)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
data = {'project_name': "test_project2", 'email': "test@example.com"} data = {
'project_name': "test_project2",
'email': "test@example.com",
'region': 'RegionOne',
}
response = self.client.put(url, data, format='json', response = self.client.put(url, data, format='json',
headers=headers) headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -844,6 +850,8 @@ class AdminAPITests(APITestCase):
@mock.patch( @mock.patch(
'stacktask.actions.models.user_store.IdentityManager', FakeManager) 'stacktask.actions.models.user_store.IdentityManager', FakeManager)
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
def test_task_list_filter(self): def test_task_list_filter(self):
""" """
""" """

View File

@ -285,7 +285,10 @@ class TaskViewTests(APITestCase):
response = self.client.post(url, {'approved': True}, format='json', response = self.client.post(url, {'approved': True}, format='json',
headers=headers) headers=headers)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'errors': ['actions invalid']}) self.assertEqual(
response.data,
{'errors': ['Cannot approve an invalid task. ' +
'Update data and rerun pre_approve.']})
@mock.patch('stacktask.actions.models.user_store.IdentityManager', @mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager) FakeManager)
@ -522,7 +525,7 @@ class TaskViewTests(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'notes': ['created token']}) self.assertEqual(response.data, {'notes': ['created token']})
response = self.client.post(url, data, format='json', headers=headers) response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
data = {'email': "test2@example.com", 'roles': ["_member_"], data = {'email': "test2@example.com", 'roles': ["_member_"],
'project_id': 'test_project_id'} 'project_id': 'test_project_id'}
@ -530,7 +533,7 @@ class TaskViewTests(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'notes': ['created token']}) self.assertEqual(response.data, {'notes': ['created token']})
response = self.client.post(url, data, format='json', headers=headers) response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
@mock.patch('stacktask.actions.models.user_store.IdentityManager', @mock.patch('stacktask.actions.models.user_store.IdentityManager',
FakeManager) FakeManager)

View File

@ -215,8 +215,6 @@ class TaskDetail(APIViewWithLogger):
""" """
Allows the updating of action data and retriggering Allows the updating of action data and retriggering
of the pre_approve step. of the pre_approve step.
Will undo task approval, and clear tokens for the task.
""" """
try: try:
task = Task.objects.get(uuid=uuid) task = Task.objects.get(uuid=uuid)
@ -321,73 +319,121 @@ class TaskDetail(APIViewWithLogger):
{'errors': ['No task with this id.']}, {'errors': ['No task with this id.']},
status=404) status=404)
if request.data.get('approved', False) is True: if request.data.get('approved') is not True:
return Response(
{'approved': ["this is a required boolean field."]},
status=400)
if task.completed: if task.completed:
return Response( return Response(
{'errors': {'errors':
['This task has already been completed.']}, ['This task has already been completed.']},
status=400) status=400)
if task.cancelled: if task.cancelled:
return Response( return Response(
{'errors': {'errors':
['This task has been cancelled.']}, ['This task has been cancelled.']},
status=400) status=400)
need_token = False # we check that the task is valid before approving it:
valid = True valid = True
for action in task.actions:
if not action.valid:
valid = False
actions = [] if not valid:
return Response(
{'errors':
['Cannot approve an invalid task. ' +
'Update data and rerun pre_approve.']},
status=400)
for action in task.actions: # We approve the task before running actions,
act_model = action.get_action() # that way if something goes wrong we know if it was approved,
actions.append(act_model) # 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:
notes = {
'errors':
[("Error: '%s' while approving task. " +
"See task itself for details.") % e],
'task': task.uuid
}
create_notification(task, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
return Response(notes, status=500)
if not action.valid:
valid = False
if action.need_token:
need_token = True
if valid:
if need_token:
token = create_token(task)
try: try:
act_model.post_approve() class_conf = settings.TASK_SETTINGS.get(
except Exception as e: 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_email(task, email_conf, token)
return Response({'notes': ['created token']},
status=200)
except KeyError as e:
notes = { notes = {
'errors': 'errors':
[("Error: '%s' while approving task. " + [("Error: '%s' while sending " +
"See task itself for details.") % e], "token. See task " +
"itself for details.") % e],
'task': task.uuid 'task': task.uuid
} }
create_notification(task, notes) create_notification(task, notes)
import traceback import traceback
trace = traceback.format_exc() trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" + self.logger.critical(("(%s) - Exception escaped!" +
"Trace: \n%s") % " %s\n Trace: \n%s") %
(timezone.now(), e, trace)) (timezone.now(), e, trace))
return Response(notes, status=500) response_dict = {
'errors':
if not action.valid: ["Error: Something went wrong on the " +
valid = False "server. It will be looked into shortly."]
if action.need_token: }
need_token = True return Response(response_dict, status=500)
else:
if valid: for action in actions:
task.approved = True
task.approved_on = timezone.now()
task.save()
if need_token:
token = create_token(task)
try: try:
class_conf = settings.TASK_SETTINGS.get( action.submit({})
task.task_type, settings.DEFAULT_TASK_SETTINGS) except Exception as e:
# will throw a key error if the token template has not
# been specified
email_conf = class_conf['emails']['token']
send_email(task, email_conf, token)
return Response({'notes': ['created token']},
status=200)
except KeyError as e:
notes = { notes = {
'errors': 'errors':
[("Error: '%s' while sending " + [("Error: '%s' while submitting " +
"token. See task " + "task. See task " +
"itself for details.") % e], "itself for details.") % e],
'task': task.uuid 'task': task.uuid
} }
@ -399,52 +445,23 @@ class TaskDetail(APIViewWithLogger):
" %s\n Trace: \n%s") % " %s\n Trace: \n%s") %
(timezone.now(), e, trace)) (timezone.now(), e, trace))
response_dict = { return Response(notes, status=500)
'errors':
["Error: Something went wrong on the " +
"server. It will be looked into shortly."]
}
return Response(response_dict, status=500)
else:
for action in actions:
try:
action.submit({})
except Exception as e:
notes = {
'errors':
[("Error: '%s' while submitting " +
"task. See task " +
"itself for details.") % e],
'task': task.uuid
}
create_notification(task, notes)
import traceback task.completed = True
trace = traceback.format_exc() task.completed_on = timezone.now()
self.logger.critical(("(%s) - Exception escaped!" + task.save()
" %s\n Trace: \n%s") %
(timezone.now(), e, trace))
return Response(notes, status=500) # 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_email(task, email_conf)
task.completed = True return Response(
task.completed_on = timezone.now() {'notes': "Task completed successfully."},
task.save() status=200)
return Response({'errors': ['actions invalid']}, status=400)
# 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_email(task, email_conf)
return Response(
{'notes': "Task completed successfully."},
status=200)
return Response({'errors': ['actions invalid']}, status=400)
else:
return Response({'approved': ["this field is required."]},
status=400)
@utils.mod_or_admin @utils.mod_or_admin
def delete(self, request, uuid, format=None): def delete(self, request, uuid, format=None):

View File

@ -151,8 +151,6 @@ USERNAME_IS_EMAIL = CONFIG['USERNAME_IS_EMAIL']
# Keystone admin credentials: # Keystone admin credentials:
KEYSTONE = CONFIG['KEYSTONE'] KEYSTONE = CONFIG['KEYSTONE']
DEFAULT_REGION = CONFIG['DEFAULT_REGION']
TOKEN_SUBMISSION_URL = CONFIG['TOKEN_SUBMISSION_URL'] TOKEN_SUBMISSION_URL = CONFIG['TOKEN_SUBMISSION_URL']
TOKEN_EXPIRE_TIME = CONFIG['TOKEN_EXPIRE_TIME'] TOKEN_EXPIRE_TIME = CONFIG['TOKEN_EXPIRE_TIME']

View File

@ -63,11 +63,9 @@ KEYSTONE = {
'username': 'admin', 'username': 'admin',
'password': 'openstack', 'password': 'openstack',
'project_name': 'admin', 'project_name': 'admin',
'auth_url': "http://localhost:5000/v3" 'auth_url': "http://localhost:5000/v3",
} }
DEFAULT_REGION = 'RegionOne'
TOKEN_SUBMISSION_URL = 'http://localhost:8080/token/' TOKEN_SUBMISSION_URL = 'http://localhost:8080/token/'
TOKEN_EXPIRE_TIME = 24 TOKEN_EXPIRE_TIME = 24
@ -120,10 +118,12 @@ TASK_SETTINGS = {
} }
}, },
'create_project': { 'create_project': {
'actions': [ 'additional_actions': [
'AddAdminToProject', 'AddDefaultUsersToProject',
'DefaultProjectResources' 'NewProjectDefaultNetwork'
] ],
'default_region': 'RegionOne',
'default_parent_id': None,
}, },
'reset_password': { 'reset_password': {
'handle_duplicates': 'cancel', 'handle_duplicates': 'cancel',
@ -154,13 +154,18 @@ TASK_SETTINGS = {
} }
ACTION_SETTINGS = { ACTION_SETTINGS = {
'NewProject': {
'default_roles': {
"project_admin", "project_mod", "_member_", "heat_stack_owner"
},
},
'NewUser': { 'NewUser': {
'allowed_roles': ['project_mod', 'project_admin', "_member_"] 'allowed_roles': ['project_mod', 'project_admin', "_member_"]
}, },
'ResetUser': { 'ResetUser': {
'blacklisted_roles': ['admin'] 'blacklisted_roles': ['admin']
}, },
'DefaultProjectResources': { 'NewDefaultNetwork': {
'RegionOne': { 'RegionOne': {
'DNS_NAMESERVERS': ['193.168.1.2', '193.168.1.3'], 'DNS_NAMESERVERS': ['193.168.1.2', '193.168.1.3'],
'SUBNET_CIDR': '192.168.1.0/24', 'SUBNET_CIDR': '192.168.1.0/24',
@ -169,7 +174,16 @@ ACTION_SETTINGS = {
'router_name': 'somerouter', 'router_name': 'somerouter',
'subnet_name': 'somesubnet' 'subnet_name': 'somesubnet'
} }
},
'AddDefaultUsersToProject': {
'default_users': [
'admin',
],
'default_roles': [
'admin',
],
} }
} }
ROLES_MAPPING = { ROLES_MAPPING = {
@ -195,7 +209,6 @@ conf_dict = {
"EMAIL_SETTINGS": EMAIL_SETTINGS, "EMAIL_SETTINGS": EMAIL_SETTINGS,
"USERNAME_IS_EMAIL": USERNAME_IS_EMAIL, "USERNAME_IS_EMAIL": USERNAME_IS_EMAIL,
"KEYSTONE": KEYSTONE, "KEYSTONE": KEYSTONE,
"DEFAULT_REGION": DEFAULT_REGION,
"ACTIVE_TASKVIEWS": ACTIVE_TASKVIEWS, "ACTIVE_TASKVIEWS": ACTIVE_TASKVIEWS,
"DEFAULT_TASK_SETTINGS": DEFAULT_TASK_SETTINGS, "DEFAULT_TASK_SETTINGS": DEFAULT_TASK_SETTINGS,
"TASK_SETTINGS": TASK_SETTINGS, "TASK_SETTINGS": TASK_SETTINGS,

View File

@ -40,8 +40,8 @@ conf = {
'username': settings.KEYSTONE['username'], 'username': settings.KEYSTONE['username'],
'password': settings.KEYSTONE['password'], 'password': settings.KEYSTONE['password'],
'project_name': settings.KEYSTONE['project_name'], 'project_name': settings.KEYSTONE['project_name'],
"project_domain_name": settings.KEYSTONE.get('domain_name', "default"), "project_domain_id": settings.KEYSTONE.get('domain_id', "default"),
"user_domain_name": settings.KEYSTONE.get('domain_name', "default"), "user_domain_id": settings.KEYSTONE.get('domain_id', "default"),
"auth_url": settings.KEYSTONE['auth_url'], "auth_url": settings.KEYSTONE['auth_url'],
'delay_auth_decision': True, 'delay_auth_decision': True,
'include_service_catalog': False, 'include_service_catalog': False,