Reformat code using the Black style linter
Change-Id: I55cf016fe073e92fe4466f38b95ebdcd9ec58e84
This commit is contained in:
parent
592e24170e
commit
2c62daf542
@ -9,23 +9,36 @@ import jsonfield.fields
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0001_initial'),
|
||||
("api", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Action',
|
||||
name="Action",
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('action_name', models.CharField(max_length=200)),
|
||||
('action_data', jsonfield.fields.JSONField(default={})),
|
||||
('cache', jsonfield.fields.JSONField(default={})),
|
||||
('state', models.CharField(default=b'default', max_length=200)),
|
||||
('valid', models.BooleanField(default=False)),
|
||||
('need_token', models.BooleanField(default=False)),
|
||||
('order', models.IntegerField()),
|
||||
('created', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Task')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID",
|
||||
serialize=False,
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
),
|
||||
),
|
||||
("action_name", models.CharField(max_length=200)),
|
||||
("action_data", jsonfield.fields.JSONField(default={})),
|
||||
("cache", jsonfield.fields.JSONField(default={})),
|
||||
("state", models.CharField(default=b"default", max_length=200)),
|
||||
("valid", models.BooleanField(default=False)),
|
||||
("need_token", models.BooleanField(default=False)),
|
||||
("order", models.IntegerField()),
|
||||
("created", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
(
|
||||
"task",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.Task"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
@ -7,13 +7,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('actions', '0001_initial'),
|
||||
("actions", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='action',
|
||||
name='auto_approve',
|
||||
model_name="action",
|
||||
name="auto_approve",
|
||||
field=models.NullBooleanField(default=None),
|
||||
),
|
||||
]
|
||||
|
@ -8,13 +8,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('actions', '0002_action_auto_approve'),
|
||||
("actions", "0002_action_auto_approve"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='action',
|
||||
name='state',
|
||||
field=models.CharField(default='default', max_length=200),
|
||||
model_name="action",
|
||||
name="state",
|
||||
field=models.CharField(default="default", max_length=200),
|
||||
),
|
||||
]
|
||||
|
@ -9,14 +9,16 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tasks', '0001_initial'),
|
||||
('actions', '0003_auto_20190610_0205'),
|
||||
("tasks", "0001_initial"),
|
||||
("actions", "0003_auto_20190610_0205"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='action',
|
||||
name='task',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.Task'),
|
||||
model_name="action",
|
||||
name="task",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="tasks.Task"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -24,13 +24,14 @@ class Action(models.Model):
|
||||
"""
|
||||
Database model representation of an action.
|
||||
"""
|
||||
|
||||
action_name = models.CharField(max_length=200)
|
||||
action_data = JSONField(default={})
|
||||
cache = JSONField(default={})
|
||||
state = models.CharField(max_length=200, default="default")
|
||||
valid = models.BooleanField(default=False)
|
||||
need_token = models.BooleanField(default=False)
|
||||
task = models.ForeignKey('tasks.Task', on_delete=models.CASCADE)
|
||||
task = models.ForeignKey("tasks.Task", on_delete=models.CASCADE)
|
||||
# NOTE(amelia): Auto approve is technically a ternary operator
|
||||
# If all in a task are None it will not auto approve
|
||||
# However if at least one action has it set to True it
|
||||
@ -46,5 +47,4 @@ class Action(models.Model):
|
||||
def get_action(self):
|
||||
"""Returns self as the appropriate action wrapper type."""
|
||||
data = self.action_data
|
||||
return actions.ACTION_CLASSES[self.action_name](
|
||||
data=data, action_model=self)
|
||||
return actions.ACTION_CLASSES[self.action_name](data=data, action_model=self)
|
||||
|
@ -30,7 +30,7 @@ def send_email(to_addresses, context, conf, task):
|
||||
Function for sending emails from actions
|
||||
"""
|
||||
|
||||
if not conf.get('template'):
|
||||
if not conf.get("template"):
|
||||
return
|
||||
|
||||
if not to_addresses:
|
||||
@ -40,66 +40,58 @@ def send_email(to_addresses, context, conf, task):
|
||||
elif isinstance(to_addresses, set):
|
||||
to_addresses = list(to_addresses)
|
||||
|
||||
text_template = loader.get_template(
|
||||
conf['template'],
|
||||
using='include_etc_templates')
|
||||
text_template = loader.get_template(conf["template"], using="include_etc_templates")
|
||||
|
||||
html_template = conf.get('html_template')
|
||||
html_template = conf.get("html_template")
|
||||
if html_template:
|
||||
html_template = loader.get_template(
|
||||
html_template,
|
||||
using='include_etc_templates')
|
||||
html_template, using="include_etc_templates"
|
||||
)
|
||||
|
||||
try:
|
||||
message = text_template.render(context)
|
||||
# from_email is the return-path and is distinct from the
|
||||
# message headers
|
||||
from_email = conf.get('from')
|
||||
from_email = conf.get("from")
|
||||
if not from_email:
|
||||
from_email = conf.get('reply')
|
||||
from_email = conf.get("reply")
|
||||
if not from_email:
|
||||
return
|
||||
elif "%(task_uuid)s" in from_email:
|
||||
from_email = from_email % {'task_uuid': task.uuid}
|
||||
from_email = from_email % {"task_uuid": task.uuid}
|
||||
|
||||
reply_email = conf['reply']
|
||||
reply_email = conf["reply"]
|
||||
# these are the message headers which will be visible to
|
||||
# the email client.
|
||||
headers = {
|
||||
'X-Adjutant-Task-UUID': task.uuid,
|
||||
"X-Adjutant-Task-UUID": task.uuid,
|
||||
# From needs to be set to be distinct from return-path
|
||||
'From': reply_email,
|
||||
'Reply-To': reply_email,
|
||||
"From": reply_email,
|
||||
"Reply-To": reply_email,
|
||||
}
|
||||
|
||||
email = EmailMultiAlternatives(
|
||||
conf['subject'],
|
||||
message,
|
||||
from_email,
|
||||
to_addresses,
|
||||
headers=headers,
|
||||
conf["subject"], message, from_email, to_addresses, headers=headers,
|
||||
)
|
||||
|
||||
if html_template:
|
||||
email.attach_alternative(
|
||||
html_template.render(context), "text/html")
|
||||
email.attach_alternative(html_template.render(context), "text/html")
|
||||
|
||||
email.send(fail_silently=False)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
notes = {
|
||||
'errors':
|
||||
("Error: '%s' while sending additional email for task: %s" %
|
||||
(e, task.uuid))
|
||||
"errors": (
|
||||
"Error: '%s' while sending additional email for task: %s"
|
||||
% (e, task.uuid)
|
||||
)
|
||||
}
|
||||
|
||||
notif_conf = task.config.notifications
|
||||
|
||||
if e.__class__.__name__ in notif_conf.safe_errors:
|
||||
notification = create_notification(
|
||||
task, notes, error=True,
|
||||
handlers=False)
|
||||
notification = create_notification(task, notes, error=True, handlers=False)
|
||||
notification.acknowledged = True
|
||||
notification.save()
|
||||
else:
|
||||
|
@ -64,15 +64,14 @@ class BaseAction(object):
|
||||
|
||||
config_group = None
|
||||
|
||||
def __init__(self, data, action_model=None, task=None,
|
||||
order=None):
|
||||
def __init__(self, data, action_model=None, task=None, order=None):
|
||||
"""
|
||||
Build itself around an existing database model,
|
||||
or build itself and creates a new database model.
|
||||
Sets up required data as fields.
|
||||
"""
|
||||
|
||||
self.logger = getLogger('adjutant')
|
||||
self.logger = getLogger("adjutant")
|
||||
|
||||
for field in self.required:
|
||||
field_data = data[field]
|
||||
@ -86,7 +85,7 @@ class BaseAction(object):
|
||||
action_name=self.__class__.__name__,
|
||||
action_data=data,
|
||||
task=task,
|
||||
order=order
|
||||
order=order,
|
||||
)
|
||||
action.save()
|
||||
self.action = action
|
||||
@ -141,8 +140,7 @@ class BaseAction(object):
|
||||
now = timezone.now()
|
||||
self.logger.info("(%s) - %s" % (now, note))
|
||||
note = "%s - (%s)" % (note, now)
|
||||
self.action.task.add_action_note(
|
||||
str(self), note)
|
||||
self.action.task.add_action_note(str(self), note)
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
@ -154,8 +152,7 @@ class BaseAction(object):
|
||||
return self._config
|
||||
|
||||
try:
|
||||
action_defaults = CONF.workflow.action_defaults.get(
|
||||
self.__class__.__name__)
|
||||
action_defaults = CONF.workflow.action_defaults.get(self.__class__.__name__)
|
||||
except KeyError:
|
||||
self._config = {}
|
||||
return self._config
|
||||
@ -163,7 +160,8 @@ class BaseAction(object):
|
||||
try:
|
||||
task_conf = CONF.workflow.tasks[self.action.task.task_type]
|
||||
self._config = action_defaults.overlay(
|
||||
task_conf.actions[self.__class__.__name__])
|
||||
task_conf.actions[self.__class__.__name__]
|
||||
)
|
||||
except KeyError:
|
||||
self._config = action_defaults
|
||||
return self._config
|
||||
@ -174,7 +172,8 @@ class BaseAction(object):
|
||||
except NotImplementedError:
|
||||
self.logger.warning(
|
||||
"DEPRECATED: Action '_pre_approve' stage has been renamed "
|
||||
"to 'prepare'.")
|
||||
"to 'prepare'."
|
||||
)
|
||||
return self._pre_approve()
|
||||
|
||||
def approve(self):
|
||||
@ -183,7 +182,8 @@ class BaseAction(object):
|
||||
except NotImplementedError:
|
||||
self.logger.warning(
|
||||
"DEPRECATED: Action '_post_approve' stage has been renamed "
|
||||
"to 'prepare'.")
|
||||
"to 'prepare'."
|
||||
)
|
||||
return self._post_approve()
|
||||
|
||||
def submit(self, token_data):
|
||||
@ -208,16 +208,16 @@ class ResourceMixin(object):
|
||||
def _validate_keystone_user_project_id(self):
|
||||
keystone_user = self.action.task.keystone_user
|
||||
|
||||
if keystone_user['project_id'] != self.project_id:
|
||||
self.add_note('Project id does not match keystone user project.')
|
||||
if keystone_user["project_id"] != self.project_id:
|
||||
self.add_note("Project id does not match keystone user project.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _validate_keystone_user_domain_id(self):
|
||||
keystone_user = self.action.task.keystone_user
|
||||
|
||||
if keystone_user['project_domain_id'] != self.domain_id:
|
||||
self.add_note('Domain id does not match keystone user domain.')
|
||||
if keystone_user["project_domain_id"] != self.domain_id:
|
||||
self.add_note("Domain id does not match keystone user domain.")
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -225,7 +225,7 @@ class ResourceMixin(object):
|
||||
id_manager = user_store.IdentityManager()
|
||||
domain = id_manager.get_domain(self.domain_id)
|
||||
if not domain:
|
||||
self.add_note('Domain does not exist.')
|
||||
self.add_note("Domain does not exist.")
|
||||
return False
|
||||
|
||||
return True
|
||||
@ -234,24 +234,23 @@ class ResourceMixin(object):
|
||||
# Handle an edge_case where some actions set their
|
||||
# own project_id value.
|
||||
if not self.project_id:
|
||||
self.add_note('No project_id given.')
|
||||
self.add_note("No project_id given.")
|
||||
return False
|
||||
|
||||
# Now actually check the project exists.
|
||||
id_manager = user_store.IdentityManager()
|
||||
project = id_manager.get_project(self.project_id)
|
||||
if not project:
|
||||
self.add_note('Project with id %s does not exist.' %
|
||||
self.project_id)
|
||||
self.add_note("Project with id %s does not exist." % self.project_id)
|
||||
return False
|
||||
self.add_note('Project with id %s exists.' % self.project_id)
|
||||
self.add_note("Project with id %s exists." % self.project_id)
|
||||
return True
|
||||
|
||||
def _validate_domain_name(self):
|
||||
id_manager = user_store.IdentityManager()
|
||||
self.domain = id_manager.find_domain(self.domain_name)
|
||||
if not self.domain:
|
||||
self.add_note('Domain does not exist.')
|
||||
self.add_note("Domain does not exist.")
|
||||
return False
|
||||
# also store the domain_id separately for later use
|
||||
self.domain_id = self.domain.id
|
||||
@ -262,9 +261,9 @@ class ResourceMixin(object):
|
||||
id_manager = user_store.IdentityManager()
|
||||
v_region = id_manager.get_region(region)
|
||||
if not v_region:
|
||||
self.add_note('ERROR: Region: %s does not exist.' % region)
|
||||
self.add_note("ERROR: Region: %s does not exist." % region)
|
||||
return False
|
||||
self.add_note('Region: %s exists.' % region)
|
||||
self.add_note("Region: %s exists." % region)
|
||||
|
||||
return True
|
||||
|
||||
@ -278,16 +277,17 @@ class UserMixin(ResourceMixin):
|
||||
|
||||
self.user = id_manager.find_user(self.username, self.domain.id)
|
||||
if not self.user:
|
||||
self.add_note('No user present with username')
|
||||
self.add_note("No user present with username")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _validate_role_permissions(self):
|
||||
keystone_user = self.action.task.keystone_user
|
||||
# Role permissions check
|
||||
if not self.are_roles_manageable(user_roles=keystone_user['roles'],
|
||||
requested_roles=self.roles):
|
||||
self.add_note('User does not have permission to edit role(s).')
|
||||
if not self.are_roles_manageable(
|
||||
user_roles=keystone_user["roles"], requested_roles=self.roles
|
||||
):
|
||||
self.add_note("User does not have permission to edit role(s).")
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -299,7 +299,7 @@ class UserMixin(ResourceMixin):
|
||||
|
||||
requested_roles = set(requested_roles)
|
||||
# blacklist checks
|
||||
blacklisted_roles = set(['admin'])
|
||||
blacklisted_roles = set(["admin"])
|
||||
if len(blacklisted_roles & requested_roles) > 0:
|
||||
return False
|
||||
|
||||
@ -317,15 +317,16 @@ class UserMixin(ResourceMixin):
|
||||
# Mutators
|
||||
def grant_roles(self, user, roles, project_id, inherited=False):
|
||||
return self._user_roles_edit(
|
||||
user, roles, project_id, remove=False, inherited=inherited)
|
||||
user, roles, project_id, remove=False, inherited=inherited
|
||||
)
|
||||
|
||||
def remove_roles(self, user, roles, project_id, inherited=False):
|
||||
return self._user_roles_edit(
|
||||
user, roles, project_id, remove=True, inherited=inherited)
|
||||
user, roles, project_id, remove=True, inherited=inherited
|
||||
)
|
||||
|
||||
# Helper function to add or remove roles
|
||||
def _user_roles_edit(self, user, roles, project_id, remove=False,
|
||||
inherited=False):
|
||||
def _user_roles_edit(self, user, roles, project_id, remove=False, inherited=False):
|
||||
id_manager = user_store.IdentityManager()
|
||||
if not remove:
|
||||
action_fn = id_manager.add_user_role
|
||||
@ -346,8 +347,9 @@ class UserMixin(ResourceMixin):
|
||||
action_fn(user, role, project_id, inherited=inherited)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while %s the roles: %s on user: %s " %
|
||||
(e, action_string, roles, user))
|
||||
"Error: '%s' while %s the roles: %s on user: %s "
|
||||
% (e, action_string, roles, user)
|
||||
)
|
||||
raise
|
||||
|
||||
def enable_user(self, user=None):
|
||||
@ -358,22 +360,27 @@ class UserMixin(ResourceMixin):
|
||||
id_manager.enable_user(user)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while re-enabling user: %s with roles: %s" %
|
||||
(e, self.username, self.roles))
|
||||
"Error: '%s' while re-enabling user: %s with roles: %s"
|
||||
% (e, self.username, self.roles)
|
||||
)
|
||||
raise
|
||||
|
||||
def create_user(self, password):
|
||||
id_manager = user_store.IdentityManager()
|
||||
try:
|
||||
user = id_manager.create_user(
|
||||
name=self.username, password=password,
|
||||
email=self.email, domain=self.domain_id,
|
||||
created_on=str_datetime(timezone.now()))
|
||||
name=self.username,
|
||||
password=password,
|
||||
email=self.email,
|
||||
domain=self.domain_id,
|
||||
created_on=str_datetime(timezone.now()),
|
||||
)
|
||||
except Exception as e:
|
||||
# TODO: Narrow the Exceptions caught to a relevant set.
|
||||
self.add_note(
|
||||
"Error: '%s' while creating user: %s with roles: %s" %
|
||||
(e, self.username, self.roles))
|
||||
"Error: '%s' while creating user: %s with roles: %s"
|
||||
% (e, self.username, self.roles)
|
||||
)
|
||||
raise
|
||||
return user
|
||||
|
||||
@ -385,8 +392,8 @@ class UserMixin(ResourceMixin):
|
||||
id_manager.update_user_password(user, password)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while changing password for user: %s" %
|
||||
(e, self.username))
|
||||
"Error: '%s' while changing password for user: %s" % (e, self.username)
|
||||
)
|
||||
raise
|
||||
|
||||
def update_email(self, email, user=None):
|
||||
@ -397,8 +404,8 @@ class UserMixin(ResourceMixin):
|
||||
id_manager.update_user_email(user, email)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while changing email for user: %s" %
|
||||
(e, self.username))
|
||||
"Error: '%s' while changing email for user: %s" % (e, self.username)
|
||||
)
|
||||
raise
|
||||
|
||||
def update_user_name(self, username, user=None):
|
||||
@ -409,8 +416,8 @@ class UserMixin(ResourceMixin):
|
||||
id_manager.update_user_name(user, username)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while changing username for user: %s" %
|
||||
(e, self.username))
|
||||
"Error: '%s' while changing username for user: %s" % (e, self.username)
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@ -424,22 +431,18 @@ class ProjectMixin(ResourceMixin):
|
||||
if self.parent_id:
|
||||
parent = id_manager.get_project(self.parent_id)
|
||||
if not parent:
|
||||
self.add_note("Parent id: '%s' does not exist." %
|
||||
self.parent_id)
|
||||
self.add_note("Parent id: '%s' does not exist." % self.parent_id)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _validate_project_absent(self):
|
||||
id_manager = user_store.IdentityManager()
|
||||
project = id_manager.find_project(
|
||||
self.project_name, self.domain_id)
|
||||
project = id_manager.find_project(self.project_name, self.domain_id)
|
||||
if project:
|
||||
self.add_note("Existing project with name '%s'." %
|
||||
self.project_name)
|
||||
self.add_note("Existing project with name '%s'." % self.project_name)
|
||||
return False
|
||||
|
||||
self.add_note("No existing project with name '%s'." %
|
||||
self.project_name)
|
||||
self.add_note("No existing project with name '%s'." % self.project_name)
|
||||
return True
|
||||
|
||||
def _create_project(self):
|
||||
@ -447,17 +450,20 @@ class ProjectMixin(ResourceMixin):
|
||||
description = getattr(self, "description", "")
|
||||
try:
|
||||
project = id_manager.create_project(
|
||||
self.project_name, created_on=str_datetime(timezone.now()),
|
||||
parent=self.parent_id, domain=self.domain_id,
|
||||
description=description)
|
||||
self.project_name,
|
||||
created_on=str_datetime(timezone.now()),
|
||||
parent=self.parent_id,
|
||||
domain=self.domain_id,
|
||||
description=description,
|
||||
)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while creating project: %s" %
|
||||
(e, self.project_name))
|
||||
"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.action.task.cache["project_id"] = project.id
|
||||
self.set_cache("project_id", project.id)
|
||||
self.add_note("New project '%s' created." % project.name)
|
||||
|
||||
|
||||
@ -477,7 +483,8 @@ class QuotaMixin(ResourceMixin):
|
||||
def _usage_greater_than_quota(self, regions):
|
||||
quota_manager = QuotaManager(
|
||||
self.project_id,
|
||||
size_difference_threshold=self.config.size_difference_threshold)
|
||||
size_difference_threshold=self.config.size_difference_threshold,
|
||||
)
|
||||
quota = CONF.quota.sizes.get(self.size, {})
|
||||
for region in regions:
|
||||
current_usage = quota_manager.get_current_usage(region)
|
||||
@ -500,7 +507,6 @@ class QuotaMixin(ResourceMixin):
|
||||
|
||||
|
||||
class UserIdAction(BaseAction):
|
||||
|
||||
def _get_target_user(self):
|
||||
"""
|
||||
Gets the target user by id
|
||||
@ -522,7 +528,7 @@ class UserNameAction(BaseAction):
|
||||
# NOTE(amelia): Make a copy to avoid editing it globally.
|
||||
self.required = list(self.required)
|
||||
try:
|
||||
self.required.remove('username')
|
||||
self.required.remove("username")
|
||||
except ValueError:
|
||||
pass
|
||||
# nothing to remove
|
||||
|
@ -32,35 +32,40 @@ def _build_default_email_group(group_name):
|
||||
fields.StrConfig(
|
||||
"subject",
|
||||
help_text="Email subject for this stage.",
|
||||
default="Openstack Email Notification")
|
||||
default="Openstack Email Notification",
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"from",
|
||||
help_text="From email for this stage.",
|
||||
regex=constants.EMAIL_WITH_TEMPLATE_REGEX,
|
||||
default="bounce+%(task_uuid)s@example.com")
|
||||
default="bounce+%(task_uuid)s@example.com",
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"reply",
|
||||
help_text="Reply-to email for this stage.",
|
||||
regex=constants.EMAIL_WITH_TEMPLATE_REGEX,
|
||||
default="no-reply@example.com")
|
||||
default="no-reply@example.com",
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"template",
|
||||
help_text="Email template for this stage. "
|
||||
"No template will cause the email not to send.",
|
||||
default=None)
|
||||
"No template will cause the email not to send.",
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"html_template",
|
||||
help_text="Email html template for this stage. "
|
||||
"No template will cause the email not to send.",
|
||||
default=None)
|
||||
"No template will cause the email not to send.",
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.BoolConfig(
|
||||
@ -108,24 +113,27 @@ class SendAdditionalEmailAction(BaseAction):
|
||||
|
||||
def set_email(self, conf):
|
||||
self.emails = set()
|
||||
if conf.get('email_current_user'):
|
||||
if conf.get("email_current_user"):
|
||||
self.add_note("Adding the current user's email address")
|
||||
if CONF.identity.username_is_email:
|
||||
self.emails.add(self.action.task.keystone_user['username'])
|
||||
self.emails.add(self.action.task.keystone_user["username"])
|
||||
else:
|
||||
try:
|
||||
id_manager = user_store.IdentityManager()
|
||||
email = id_manager.get_user(
|
||||
self.action.task.keystone_user['user_id']).email
|
||||
self.action.task.keystone_user["user_id"]
|
||||
).email
|
||||
self.emails.add(email)
|
||||
except AttributeError:
|
||||
self.add_note("Could not add current user email address")
|
||||
|
||||
if conf.get('email_roles'):
|
||||
roles = set(conf.get('email_roles'))
|
||||
project_id = self.action.task.keystone_user['project_id']
|
||||
self.add_note('Adding email addresses for roles %s in project %s'
|
||||
% (roles, project_id))
|
||||
if conf.get("email_roles"):
|
||||
roles = set(conf.get("email_roles"))
|
||||
project_id = self.action.task.keystone_user["project_id"]
|
||||
self.add_note(
|
||||
"Adding email addresses for roles %s in project %s"
|
||||
% (roles, project_id)
|
||||
)
|
||||
|
||||
id_manager = user_store.IdentityManager()
|
||||
users = id_manager.list_users(project_id)
|
||||
@ -137,14 +145,14 @@ class SendAdditionalEmailAction(BaseAction):
|
||||
else:
|
||||
self.emails.add(user.email)
|
||||
|
||||
if conf.get('email_task_cache'):
|
||||
task_emails = self.action.task.cache.get('additional_emails', [])
|
||||
if conf.get("email_task_cache"):
|
||||
task_emails = self.action.task.cache.get("additional_emails", [])
|
||||
if isinstance(task_emails, six.string_types):
|
||||
task_emails = [task_emails]
|
||||
for email in task_emails:
|
||||
self.emails.add(email)
|
||||
|
||||
for email in conf.get('email_additional_addresses'):
|
||||
for email in conf.get("email_additional_addresses"):
|
||||
self.emails.add(email)
|
||||
|
||||
def _validate(self):
|
||||
@ -152,13 +160,13 @@ class SendAdditionalEmailAction(BaseAction):
|
||||
self.action.save()
|
||||
|
||||
def _prepare(self):
|
||||
self.perform_action('prepare')
|
||||
self.perform_action("prepare")
|
||||
|
||||
def _approve(self):
|
||||
self.perform_action('approve')
|
||||
self.perform_action("approve")
|
||||
|
||||
def _submit(self, data):
|
||||
self.perform_action('submit')
|
||||
self.perform_action("submit")
|
||||
|
||||
def perform_action(self, stage):
|
||||
self._validate()
|
||||
@ -171,7 +179,7 @@ class SendAdditionalEmailAction(BaseAction):
|
||||
email_conf = self.config.get(stage)
|
||||
|
||||
# If either of these are false we won't be sending anything.
|
||||
if not email_conf or not email_conf.get('template'):
|
||||
if not email_conf or not email_conf.get("template"):
|
||||
return
|
||||
|
||||
self.set_email(email_conf)
|
||||
@ -188,10 +196,7 @@ class SendAdditionalEmailAction(BaseAction):
|
||||
act = action.get_action()
|
||||
actions[str(act)] = act
|
||||
|
||||
context = {
|
||||
'task': task,
|
||||
'actions': actions
|
||||
}
|
||||
context = {"task": task, "actions": actions}
|
||||
|
||||
result = send_email(self.emails, context, email_conf, task)
|
||||
|
||||
|
@ -23,8 +23,7 @@ from adjutant.config import CONF
|
||||
from adjutant.common import user_store
|
||||
from adjutant.common.utils import str_datetime
|
||||
from adjutant.actions.utils import validate_steps
|
||||
from adjutant.actions.v1.base import (
|
||||
BaseAction, UserNameAction, UserMixin, ProjectMixin)
|
||||
from adjutant.actions.v1.base import BaseAction, UserNameAction, UserMixin, ProjectMixin
|
||||
from adjutant.actions.v1 import serializers
|
||||
|
||||
|
||||
@ -36,10 +35,10 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
"""
|
||||
|
||||
required = [
|
||||
'domain_id',
|
||||
'parent_id',
|
||||
'project_name',
|
||||
'description',
|
||||
"domain_id",
|
||||
"parent_id",
|
||||
"project_name",
|
||||
"description",
|
||||
]
|
||||
|
||||
serializer = serializers.NewProjectSerializer
|
||||
@ -50,7 +49,7 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
"default_roles",
|
||||
help_text="Roles to be given on project to the creating user.",
|
||||
default=[],
|
||||
sample_default=["member", "project_admin"]
|
||||
sample_default=["member", "project_admin"],
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -59,18 +58,20 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
super(NewProjectAction, self).__init__(*args, **kwargs)
|
||||
|
||||
def _validate(self):
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_domain_id,
|
||||
self._validate_keystone_user_parent_project,
|
||||
self._validate_project_absent,
|
||||
])
|
||||
self.action.valid = validate_steps(
|
||||
[
|
||||
self._validate_domain_id,
|
||||
self._validate_keystone_user_parent_project,
|
||||
self._validate_project_absent,
|
||||
]
|
||||
)
|
||||
self.action.save()
|
||||
|
||||
def _validate_domain_id(self):
|
||||
keystone_user = self.action.task.keystone_user
|
||||
|
||||
if keystone_user['project_domain_id'] != self.domain_id:
|
||||
self.add_note('Domain id does not match keystone user domain.')
|
||||
if keystone_user["project_domain_id"] != self.domain_id:
|
||||
self.add_note("Domain id does not match keystone user domain.")
|
||||
return False
|
||||
|
||||
return super(NewProjectAction, self)._validate_domain_id()
|
||||
@ -79,9 +80,8 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
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.')
|
||||
if self.parent_id != keystone_user["project_id"]:
|
||||
self.add_note("Parent id does not match keystone user project.")
|
||||
return False
|
||||
return self._validate_parent_project()
|
||||
return True
|
||||
@ -90,9 +90,9 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
self._validate()
|
||||
|
||||
def _approve(self):
|
||||
project_id = self.get_cache('project_id')
|
||||
project_id = self.get_cache("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.")
|
||||
else:
|
||||
self._validate()
|
||||
@ -102,34 +102,38 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
|
||||
self._create_project()
|
||||
|
||||
user_id = self.get_cache('user_id')
|
||||
user_id = self.get_cache("user_id")
|
||||
if user_id:
|
||||
self.action.task.cache['user_id'] = user_id
|
||||
self.action.task.cache["user_id"] = user_id
|
||||
self.add_note("User already given roles.")
|
||||
else:
|
||||
default_roles = self.config.default_roles
|
||||
|
||||
project_id = self.get_cache('project_id')
|
||||
project_id = self.get_cache("project_id")
|
||||
keystone_user = self.action.task.keystone_user
|
||||
|
||||
try:
|
||||
id_manager = user_store.IdentityManager()
|
||||
user = id_manager.get_user(keystone_user['user_id'])
|
||||
user = 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, default_roles, user.name, project_id))
|
||||
(
|
||||
"Error: '%s' while adding roles %s "
|
||||
"to user '%s' on project '%s'"
|
||||
)
|
||||
% (e, default_roles, user.name, 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.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"
|
||||
% (user.name, project_id, default_roles))
|
||||
% (user.name, project_id, default_roles)
|
||||
)
|
||||
|
||||
def _submit(self, token_data):
|
||||
"""
|
||||
@ -144,13 +148,7 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
doesn't exists.
|
||||
"""
|
||||
|
||||
required = [
|
||||
'domain_id',
|
||||
'parent_id',
|
||||
'project_name',
|
||||
'username',
|
||||
'email'
|
||||
]
|
||||
required = ["domain_id", "parent_id", "project_name", "username", "email"]
|
||||
|
||||
serializer = serializers.NewProjectWithUserSerializer
|
||||
|
||||
@ -160,7 +158,7 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
"default_roles",
|
||||
help_text="Roles to be given on project for the user.",
|
||||
default=[],
|
||||
sample_default=["member", "project_admin"]
|
||||
sample_default=["member", "project_admin"],
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -169,12 +167,14 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
super(NewProjectWithUserAction, self).__init__(*args, **kwargs)
|
||||
|
||||
def _validate(self):
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_domain_id,
|
||||
self._validate_parent_project,
|
||||
self._validate_project_absent,
|
||||
self._validate_user,
|
||||
])
|
||||
self.action.valid = validate_steps(
|
||||
[
|
||||
self._validate_domain_id,
|
||||
self._validate_parent_project,
|
||||
self._validate_project_absent,
|
||||
self._validate_user,
|
||||
]
|
||||
)
|
||||
self.action.save()
|
||||
|
||||
def _validate_user(self):
|
||||
@ -184,36 +184,40 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
if not user:
|
||||
self.add_note(
|
||||
"No user present with username '%s'. "
|
||||
"Need to create new user." % self.username)
|
||||
"Need to create new user." % self.username
|
||||
)
|
||||
if not id_manager.can_edit_users:
|
||||
self.add_note(
|
||||
'Identity backend does not support user editing, '
|
||||
'cannot create new user.')
|
||||
"Identity backend does not support user editing, "
|
||||
"cannot create new user."
|
||||
)
|
||||
return False
|
||||
# add to cache to use in template
|
||||
self.action.task.cache['user_state'] = "default"
|
||||
self.action.task.cache["user_state"] = "default"
|
||||
self.action.need_token = True
|
||||
self.set_token_fields(["password"])
|
||||
return True
|
||||
|
||||
if (not CONF.identity.username_is_email
|
||||
and getattr(user, 'email', None) != self.email):
|
||||
self.add_note("Existing user '%s' with non-matching email." %
|
||||
self.username)
|
||||
if (
|
||||
not CONF.identity.username_is_email
|
||||
and getattr(user, "email", None) != self.email
|
||||
):
|
||||
self.add_note("Existing user '%s' with non-matching email." % self.username)
|
||||
return False
|
||||
|
||||
if not user.enabled:
|
||||
self.add_note(
|
||||
"Existing disabled user '%s' with matching email." %
|
||||
self.email)
|
||||
"Existing disabled user '%s' with matching email." % self.email
|
||||
)
|
||||
if not id_manager.can_edit_users:
|
||||
self.add_note(
|
||||
'Identity backend does not support user editing, '
|
||||
'cannot renable user.')
|
||||
"Identity backend does not support user editing, "
|
||||
"cannot renable user."
|
||||
)
|
||||
return False
|
||||
self.action.state = "disabled"
|
||||
# add to cache to use in template
|
||||
self.action.task.cache['user_state'] = "disabled"
|
||||
self.action.task.cache["user_state"] = "disabled"
|
||||
self.action.need_token = True
|
||||
# as they are disabled we'll reset their password
|
||||
self.set_token_fields(["password"])
|
||||
@ -221,15 +225,14 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
else:
|
||||
self.action.state = "existing"
|
||||
# add to cache to use in template
|
||||
self.action.task.cache['user_state'] = "existing"
|
||||
self.action.task.cache["user_state"] = "existing"
|
||||
self.action.need_token = False
|
||||
self.add_note("Existing user '%s' with matching email." %
|
||||
self.email)
|
||||
self.add_note("Existing user '%s' with matching email." % self.email)
|
||||
return True
|
||||
|
||||
def _validate_user_submit(self):
|
||||
user_id = self.get_cache('user_id')
|
||||
project_id = self.get_cache('project_id')
|
||||
user_id = self.get_cache("user_id")
|
||||
project_id = self.get_cache("project_id")
|
||||
|
||||
id_manager = user_store.IdentityManager()
|
||||
|
||||
@ -241,7 +244,7 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
else:
|
||||
self.action.valid = False
|
||||
|
||||
self.action.task.cache['user_state'] = self.action.state
|
||||
self.action.task.cache["user_state"] = self.action.state
|
||||
|
||||
self.action.save()
|
||||
|
||||
@ -257,15 +260,16 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
if not self.valid:
|
||||
return
|
||||
|
||||
project_id = self.get_cache('project_id')
|
||||
project_id = self.get_cache("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.")
|
||||
else:
|
||||
self.action.valid = (
|
||||
self._validate_domain_id()
|
||||
and self._validate_parent_project()
|
||||
and self._validate_project_absent())
|
||||
and self._validate_project_absent()
|
||||
)
|
||||
self.action.save()
|
||||
|
||||
if not self.valid:
|
||||
@ -274,11 +278,11 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
self._create_project()
|
||||
|
||||
# User validation and checks
|
||||
user_id = self.get_cache('user_id')
|
||||
roles_granted = self.get_cache('roles_granted')
|
||||
user_id = self.get_cache("user_id")
|
||||
roles_granted = self.get_cache("roles_granted")
|
||||
if user_id and roles_granted:
|
||||
self.action.task.cache['user_id'] = user_id
|
||||
self.action.task.cache['user_state'] = self.action.state
|
||||
self.action.task.cache["user_id"] = user_id
|
||||
self.action.task.cache["user_state"] = self.action.state
|
||||
self.add_note("User already setup.")
|
||||
elif not user_id:
|
||||
self.action.valid = self._validate_user()
|
||||
@ -295,60 +299,66 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
id_manager = user_store.IdentityManager()
|
||||
default_roles = self.config.default_roles
|
||||
|
||||
project_id = self.get_cache('project_id')
|
||||
project_id = self.get_cache("project_id")
|
||||
|
||||
if self.action.state == "default":
|
||||
try:
|
||||
# Generate a temporary password:
|
||||
password = uuid4().hex + uuid4().hex
|
||||
|
||||
user_id = self.get_cache('user_id')
|
||||
user_id = self.get_cache("user_id")
|
||||
if not user_id:
|
||||
user = id_manager.create_user(
|
||||
name=self.username, password=password,
|
||||
email=self.email, domain=self.domain_id,
|
||||
created_on=str_datetime(timezone.now()))
|
||||
self.set_cache('user_id', user.id)
|
||||
name=self.username,
|
||||
password=password,
|
||||
email=self.email,
|
||||
domain=self.domain_id,
|
||||
created_on=str_datetime(timezone.now()),
|
||||
)
|
||||
self.set_cache("user_id", user.id)
|
||||
else:
|
||||
user = id_manager.get_user(user_id)
|
||||
# put user_id into action cache:
|
||||
self.action.task.cache['user_id'] = user.id
|
||||
self.action.task.cache["user_id"] = user.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))
|
||||
"Error: '%s' while creating user: %s with roles: %s"
|
||||
% (e, self.username, default_roles)
|
||||
)
|
||||
raise
|
||||
|
||||
self.set_cache('roles_granted', True)
|
||||
self.set_cache("roles_granted", True)
|
||||
self.add_note(
|
||||
"New user '%s' created for project %s with roles: %s" %
|
||||
(self.username, project_id, default_roles))
|
||||
"New user '%s' created for project %s with roles: %s"
|
||||
% (self.username, project_id, default_roles)
|
||||
)
|
||||
elif self.action.state == "existing":
|
||||
try:
|
||||
user_id = self.get_cache('user_id')
|
||||
user_id = self.get_cache("user_id")
|
||||
if not user_id:
|
||||
user = id_manager.find_user(
|
||||
self.username, self.domain_id)
|
||||
self.set_cache('user_id', user.id)
|
||||
user = id_manager.find_user(self.username, self.domain_id)
|
||||
self.set_cache("user_id", user.id)
|
||||
else:
|
||||
user = id_manager.get_user(user_id)
|
||||
self.action.task.cache['user_id'] = user.id
|
||||
self.action.task.cache["user_id"] = user.id
|
||||
|
||||
self.grant_roles(user, default_roles, project_id)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while granting roles: %s to user: %s" %
|
||||
(e, default_roles, self.username))
|
||||
"Error: '%s' while granting roles: %s to user: %s"
|
||||
% (e, default_roles, self.username)
|
||||
)
|
||||
raise
|
||||
|
||||
self.set_cache('roles_granted', True)
|
||||
self.set_cache("roles_granted", True)
|
||||
self.add_note(
|
||||
"Existing user '%s' setup on project %s with roles: %s"
|
||||
% (self.username, project_id, default_roles))
|
||||
% (self.username, project_id, default_roles)
|
||||
)
|
||||
elif self.action.state == "disabled":
|
||||
user_id = self.get_cache('user_id')
|
||||
user_id = self.get_cache("user_id")
|
||||
if not user_id:
|
||||
# first re-enable user
|
||||
try:
|
||||
@ -356,8 +366,8 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
id_manager.enable_user(user)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while re-enabling user: %s" %
|
||||
(e, self.username))
|
||||
"Error: '%s' while re-enabling user: %s" % (e, self.username)
|
||||
)
|
||||
raise
|
||||
|
||||
# and now update their password
|
||||
@ -367,32 +377,34 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
id_manager.update_user_password(user, password)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while changing password for user: %s" %
|
||||
(e, self.username))
|
||||
"Error: '%s' while changing password for user: %s"
|
||||
% (e, self.username)
|
||||
)
|
||||
raise
|
||||
self.add_note(
|
||||
'User %s password has been changed.' % self.username)
|
||||
self.add_note("User %s password has been changed." % self.username)
|
||||
|
||||
self.set_cache('user_id', user.id)
|
||||
self.set_cache("user_id", user.id)
|
||||
else:
|
||||
user = id_manager.get_user(user_id)
|
||||
self.action.task.cache['user_id'] = user.id
|
||||
self.action.task.cache["user_id"] = user.id
|
||||
|
||||
# now add their roles
|
||||
roles_granted = self.get_cache('roles_granted')
|
||||
roles_granted = self.get_cache("roles_granted")
|
||||
if not roles_granted:
|
||||
try:
|
||||
self.grant_roles(user, default_roles, project_id)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while granting user: %s roles: %s" %
|
||||
(e, self.username, default_roles))
|
||||
"Error: '%s' while granting user: %s roles: %s"
|
||||
% (e, self.username, default_roles)
|
||||
)
|
||||
raise
|
||||
self.set_cache('roles_granted', True)
|
||||
self.set_cache("roles_granted", True)
|
||||
|
||||
self.add_note(
|
||||
"Existing user '%s' setup on project %s with roles: %s"
|
||||
% (self.username, project_id, default_roles))
|
||||
% (self.username, project_id, default_roles)
|
||||
)
|
||||
|
||||
def _submit(self, token_data):
|
||||
"""
|
||||
@ -407,29 +419,30 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
if not self.valid:
|
||||
return
|
||||
|
||||
project_id = self.get_cache('project_id')
|
||||
self.action.task.cache['project_id'] = project_id
|
||||
user_id = self.get_cache('user_id')
|
||||
self.action.task.cache['user_id'] = user_id
|
||||
project_id = self.get_cache("project_id")
|
||||
self.action.task.cache["project_id"] = project_id
|
||||
user_id = self.get_cache("user_id")
|
||||
self.action.task.cache["user_id"] = user_id
|
||||
id_manager = user_store.IdentityManager()
|
||||
|
||||
if self.action.state in ["default", "disabled"]:
|
||||
user = id_manager.get_user(user_id)
|
||||
try:
|
||||
id_manager.update_user_password(
|
||||
user, token_data['password'])
|
||||
id_manager.update_user_password(user, token_data["password"])
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while changing password for user: %s" %
|
||||
(e, self.username))
|
||||
"Error: '%s' while changing password for user: %s"
|
||||
% (e, self.username)
|
||||
)
|
||||
raise
|
||||
self.add_note('User %s password has been changed.' % self.username)
|
||||
self.add_note("User %s password has been changed." % self.username)
|
||||
|
||||
elif self.action.state == "existing":
|
||||
# do nothing, everything is already done.
|
||||
self.add_note(
|
||||
"Existing user '%s' already attached to project %s" % (
|
||||
user_id, project_id))
|
||||
"Existing user '%s' already attached to project %s"
|
||||
% (user_id, project_id)
|
||||
)
|
||||
|
||||
|
||||
class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
@ -441,7 +454,7 @@ class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
"""
|
||||
|
||||
required = [
|
||||
'domain_id',
|
||||
"domain_id",
|
||||
]
|
||||
|
||||
serializer = serializers.AddDefaultUsersToProjectSerializer
|
||||
@ -472,24 +485,21 @@ class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
for user in self.users:
|
||||
ks_user = id_manager.find_user(user, self.domain_id)
|
||||
if ks_user:
|
||||
self.add_note('User: %s exists.' % user)
|
||||
self.add_note("User: %s exists." % user)
|
||||
else:
|
||||
self.add_note('ERROR: User: %s does not exist.' % user)
|
||||
self.add_note("ERROR: User: %s does not exist." % user)
|
||||
all_found = False
|
||||
|
||||
return all_found
|
||||
|
||||
def _pre_validate(self):
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_users,
|
||||
])
|
||||
self.action.valid = validate_steps([self._validate_users,])
|
||||
self.action.save()
|
||||
|
||||
def _validate(self):
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_users,
|
||||
self._validate_project_id,
|
||||
])
|
||||
self.action.valid = validate_steps(
|
||||
[self._validate_users, self._validate_project_id,]
|
||||
)
|
||||
self.action.save()
|
||||
|
||||
def _prepare(self):
|
||||
@ -497,7 +507,7 @@ class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
|
||||
def _approve(self):
|
||||
id_manager = user_store.IdentityManager()
|
||||
self.project_id = self.action.task.cache.get('project_id', None)
|
||||
self.project_id = self.action.task.cache.get("project_id", None)
|
||||
self._validate()
|
||||
|
||||
if self.valid and not self.action.state == "completed":
|
||||
@ -507,12 +517,14 @@ class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
|
||||
|
||||
self.grant_roles(ks_user, self.roles, self.project_id)
|
||||
self.add_note(
|
||||
'User: "%s" given roles: %s on project: %s.' %
|
||||
(ks_user.name, self.roles, self.project_id))
|
||||
'User: "%s" given roles: %s on project: %s.'
|
||||
% (ks_user.name, self.roles, self.project_id)
|
||||
)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while adding users to project: %s" %
|
||||
(e, self.project_id))
|
||||
"Error: '%s' while adding users to project: %s"
|
||||
% (e, self.project_id)
|
||||
)
|
||||
raise
|
||||
self.action.state = "completed"
|
||||
self.action.save()
|
||||
|
@ -36,9 +36,9 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
||||
"""
|
||||
|
||||
required = [
|
||||
'setup_network',
|
||||
'project_id',
|
||||
'region',
|
||||
"setup_network",
|
||||
"project_id",
|
||||
"region",
|
||||
]
|
||||
|
||||
serializer = serializers.NewDefaultNetworkSerializer
|
||||
@ -64,23 +64,20 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
||||
default="default_router",
|
||||
),
|
||||
fields.StrConfig(
|
||||
"public_network",
|
||||
help_text="ID of the public network.",
|
||||
"public_network", help_text="ID of the public network.",
|
||||
),
|
||||
fields.StrConfig(
|
||||
"subnet_cidr",
|
||||
help_text="CIDR for the default subnet.",
|
||||
"subnet_cidr", help_text="CIDR for the default subnet.",
|
||||
),
|
||||
fields.ListConfig(
|
||||
"dns_nameservers",
|
||||
help_text="DNS nameservers for the subnet.",
|
||||
"dns_nameservers", help_text="DNS nameservers for the subnet.",
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
fields.DictConfig(
|
||||
"regions",
|
||||
help_text="Specific per region config for default network. "
|
||||
"See 'region_defaults'.",
|
||||
"See 'region_defaults'.",
|
||||
default={},
|
||||
),
|
||||
]
|
||||
@ -91,83 +88,86 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
||||
|
||||
def _validate_region(self):
|
||||
if not self.region:
|
||||
self.add_note('ERROR: No region given.')
|
||||
self.add_note("ERROR: No region given.")
|
||||
return False
|
||||
|
||||
id_manager = user_store.IdentityManager()
|
||||
region = id_manager.get_region(self.region)
|
||||
if not region:
|
||||
self.add_note('ERROR: Region does not exist.')
|
||||
self.add_note("ERROR: Region does not exist.")
|
||||
return False
|
||||
|
||||
self.add_note('Region: %s exists.' % self.region)
|
||||
self.add_note("Region: %s exists." % self.region)
|
||||
return True
|
||||
|
||||
def _validate(self):
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_region,
|
||||
self._validate_project_id,
|
||||
self._validate_keystone_user_project_id,
|
||||
])
|
||||
self.action.valid = validate_steps(
|
||||
[
|
||||
self._validate_region,
|
||||
self._validate_project_id,
|
||||
self._validate_keystone_user_project_id,
|
||||
]
|
||||
)
|
||||
self.action.save()
|
||||
|
||||
def _create_network(self):
|
||||
neutron = openstack_clients.get_neutronclient(region=self.region)
|
||||
try:
|
||||
region_config = self.config.regions[self.region]
|
||||
network_config = self.config.region_defaults.overlay(
|
||||
region_config)
|
||||
network_config = self.config.region_defaults.overlay(region_config)
|
||||
except KeyError:
|
||||
network_config = self.config.region_defaults
|
||||
|
||||
if not self.get_cache('network_id'):
|
||||
if not self.get_cache("network_id"):
|
||||
try:
|
||||
network_body = {
|
||||
"network": {
|
||||
"name": network_config.network_name,
|
||||
'tenant_id': self.project_id,
|
||||
"admin_state_up": True
|
||||
"tenant_id": self.project_id,
|
||||
"admin_state_up": True,
|
||||
}
|
||||
}
|
||||
network = neutron.create_network(body=network_body)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while creating network: %s" %
|
||||
(e, network_config.network_name))
|
||||
"Error: '%s' while creating network: %s"
|
||||
% (e, network_config.network_name)
|
||||
)
|
||||
raise
|
||||
self.set_cache('network_id', network['network']['id'])
|
||||
self.add_note("Network %s created for project %s" %
|
||||
(network_config.network_name,
|
||||
self.project_id))
|
||||
self.set_cache("network_id", network["network"]["id"])
|
||||
self.add_note(
|
||||
"Network %s created for project %s"
|
||||
% (network_config.network_name, self.project_id)
|
||||
)
|
||||
else:
|
||||
self.add_note("Network %s already created for project %s" %
|
||||
(network_config.network_name,
|
||||
self.project_id))
|
||||
self.add_note(
|
||||
"Network %s already created for project %s"
|
||||
% (network_config.network_name, self.project_id)
|
||||
)
|
||||
|
||||
if not self.get_cache('subnet_id'):
|
||||
if not self.get_cache("subnet_id"):
|
||||
try:
|
||||
subnet_body = {
|
||||
"subnet": {
|
||||
"network_id": self.get_cache('network_id'),
|
||||
"network_id": self.get_cache("network_id"),
|
||||
"ip_version": 4,
|
||||
'tenant_id': self.project_id,
|
||||
'dns_nameservers': network_config.dns_nameservers,
|
||||
"cidr": network_config.subnet_cidr
|
||||
"tenant_id": self.project_id,
|
||||
"dns_nameservers": network_config.dns_nameservers,
|
||||
"cidr": network_config.subnet_cidr,
|
||||
}
|
||||
}
|
||||
subnet = neutron.create_subnet(body=subnet_body)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while creating subnet" % e)
|
||||
self.add_note("Error: '%s' while creating subnet" % e)
|
||||
raise
|
||||
self.set_cache('subnet_id', subnet['subnet']['id'])
|
||||
self.add_note("Subnet created for network %s" %
|
||||
network_config.network_name)
|
||||
self.set_cache("subnet_id", subnet["subnet"]["id"])
|
||||
self.add_note("Subnet created for network %s" % network_config.network_name)
|
||||
else:
|
||||
self.add_note("Subnet already created for network %s" %
|
||||
network_config.network_name)
|
||||
self.add_note(
|
||||
"Subnet already created for network %s" % network_config.network_name
|
||||
)
|
||||
|
||||
if not self.get_cache('router_id'):
|
||||
if not self.get_cache("router_id"):
|
||||
try:
|
||||
router_body = {
|
||||
"router": {
|
||||
@ -175,39 +175,35 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
|
||||
"external_gateway_info": {
|
||||
"network_id": network_config.public_network
|
||||
},
|
||||
'tenant_id': self.project_id,
|
||||
"admin_state_up": True
|
||||
"tenant_id": self.project_id,
|
||||
"admin_state_up": True,
|
||||
}
|
||||
}
|
||||
router = neutron.create_router(body=router_body)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while creating router: %s" %
|
||||
(e, network_config.router_name))
|
||||
"Error: '%s' while creating router: %s"
|
||||
% (e, network_config.router_name)
|
||||
)
|
||||
raise
|
||||
self.set_cache('router_id', router['router']['id'])
|
||||
self.add_note("Router created for project %s" %
|
||||
self.project_id)
|
||||
self.set_cache("router_id", router["router"]["id"])
|
||||
self.add_note("Router created for project %s" % self.project_id)
|
||||
else:
|
||||
self.add_note("Router already created for project %s" %
|
||||
self.project_id)
|
||||
self.add_note("Router already created for project %s" % self.project_id)
|
||||
|
||||
if not self.get_cache('port_id'):
|
||||
if not self.get_cache("port_id"):
|
||||
try:
|
||||
interface_body = {
|
||||
"subnet_id": self.get_cache('subnet_id')
|
||||
}
|
||||
interface_body = {"subnet_id": self.get_cache("subnet_id")}
|
||||
interface = neutron.add_interface_router(
|
||||
self.get_cache('router_id'), body=interface_body)
|
||||
self.get_cache("router_id"), body=interface_body
|
||||
)
|
||||
except Exception as e:
|
||||
self.add_note(
|
||||
"Error: '%s' while attaching interface" % e)
|
||||
self.add_note("Error: '%s' while attaching interface" % e)
|
||||
raise
|
||||
self.set_cache('port_id', interface['port_id'])
|
||||
self.set_cache("port_id", interface["port_id"])
|
||||
self.add_note("Interface added to router for subnet")
|
||||
else:
|
||||
self.add_note(
|
||||
"Interface added to router for project %s" % self.project_id)
|
||||
self.add_note("Interface added to router for project %s" % self.project_id)
|
||||
|
||||
def _prepare(self):
|
||||
# Note: Do we need to get this from cache? it is a required setting
|
||||
@ -231,31 +227,28 @@ class NewProjectDefaultNetworkAction(NewDefaultNetworkAction):
|
||||
"""
|
||||
|
||||
required = [
|
||||
'setup_network',
|
||||
'region',
|
||||
"setup_network",
|
||||
"region",
|
||||
]
|
||||
|
||||
serializer = serializers.NewProjectDefaultNetworkSerializer
|
||||
|
||||
def _pre_validate(self):
|
||||
# Note: Don't check project here as it doesn't exist yet.
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_region,
|
||||
])
|
||||
self.action.valid = validate_steps([self._validate_region,])
|
||||
self.action.save()
|
||||
|
||||
def _validate(self):
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_region,
|
||||
self._validate_project_id,
|
||||
])
|
||||
self.action.valid = validate_steps(
|
||||
[self._validate_region, self._validate_project_id,]
|
||||
)
|
||||
self.action.save()
|
||||
|
||||
def _prepare(self):
|
||||
self._pre_validate()
|
||||
|
||||
def _approve(self):
|
||||
self.project_id = self.action.task.cache.get('project_id', None)
|
||||
self.project_id = self.action.task.cache.get("project_id", None)
|
||||
self._validate()
|
||||
|
||||
if self.setup_network and self.valid:
|
||||
@ -266,9 +259,9 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
||||
""" Updates quota for a project to a given size in a list of regions """
|
||||
|
||||
required = [
|
||||
'size',
|
||||
'project_id',
|
||||
'regions',
|
||||
"size",
|
||||
"project_id",
|
||||
"regions",
|
||||
]
|
||||
|
||||
serializer = serializers.UpdateProjectQuotasSerializer
|
||||
@ -293,10 +286,10 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
||||
def _get_email(self):
|
||||
|
||||
if CONF.identity.username_is_email:
|
||||
return self.action.task.keystone_user['username']
|
||||
return self.action.task.keystone_user["username"]
|
||||
else:
|
||||
id_manager = user_store.IdentityManager()
|
||||
user = id_manager.users.get(self.keystone_user['user_id'])
|
||||
user = id_manager.users.get(self.keystone_user["user_id"])
|
||||
email = user.email
|
||||
if email:
|
||||
return email
|
||||
@ -316,17 +309,20 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
||||
quota_config = CONF.quota.sizes.get(quota_size, {})
|
||||
if not quota_config:
|
||||
self.add_note(
|
||||
"Project quota not defined for size '%s' in region %s." % (
|
||||
quota_size, region_name))
|
||||
"Project quota not defined for size '%s' in region %s."
|
||||
% (quota_size, region_name)
|
||||
)
|
||||
return
|
||||
|
||||
quota_manager = QuotaManager(
|
||||
self.project_id, self.config.size_difference_threshold)
|
||||
self.project_id, self.config.size_difference_threshold
|
||||
)
|
||||
|
||||
quota_manager.set_region_quota(region_name, quota_config)
|
||||
|
||||
self.add_note("Project quota for region %s set to %s" % (
|
||||
region_name, quota_size))
|
||||
self.add_note(
|
||||
"Project quota for region %s set to %s" % (region_name, quota_size)
|
||||
)
|
||||
|
||||
def _can_auto_approve(self):
|
||||
wait_days = self.config.days_between_autoapprove
|
||||
@ -334,30 +330,34 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
||||
completed_on__gte=timezone.now() - timedelta(days=wait_days),
|
||||
task_type__exact=self.action.task.task_type,
|
||||
cancelled__exact=False,
|
||||
project_id__exact=self.project_id)
|
||||
project_id__exact=self.project_id,
|
||||
)
|
||||
|
||||
changed_in_period = False
|
||||
# Check to see if there have been any updates in the relavent regions
|
||||
# recently
|
||||
for task in task_list:
|
||||
for action in task.actions:
|
||||
intersect = set(action.action_data[
|
||||
'regions']).intersection(self.regions)
|
||||
intersect = set(action.action_data["regions"]).intersection(
|
||||
self.regions
|
||||
)
|
||||
if intersect:
|
||||
changed_in_period = True
|
||||
|
||||
region_sizes = []
|
||||
|
||||
quota_manager = QuotaManager(
|
||||
self.project_id, self.config.size_difference_threshold)
|
||||
self.project_id, self.config.size_difference_threshold
|
||||
)
|
||||
|
||||
for region in self.regions:
|
||||
current_size = quota_manager.get_region_quota_data(
|
||||
region, include_usage=False)['current_quota_size']
|
||||
region, include_usage=False
|
||||
)["current_quota_size"]
|
||||
region_sizes.append(current_size)
|
||||
self.add_note(
|
||||
"Project has size '%s' in region: '%s'" %
|
||||
(current_size, region))
|
||||
"Project has size '%s' in region: '%s'" % (current_size, region)
|
||||
)
|
||||
|
||||
# Check for preapproved_quotas
|
||||
preapproved_quotas = []
|
||||
@ -365,42 +365,44 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
||||
|
||||
# If all region sizes are the same
|
||||
if region_sizes.count(region_sizes[0]) == len(region_sizes):
|
||||
preapproved_quotas = quota_manager.get_quota_change_options(
|
||||
region_sizes[0])
|
||||
smaller_quotas = quota_manager.get_smaller_quota_options(
|
||||
region_sizes[0])
|
||||
preapproved_quotas = quota_manager.get_quota_change_options(region_sizes[0])
|
||||
smaller_quotas = quota_manager.get_smaller_quota_options(region_sizes[0])
|
||||
|
||||
if self.size in smaller_quotas:
|
||||
self.add_note(
|
||||
"Quota size '%s' is in list of smaller quotas: %s" %
|
||||
(self.size, smaller_quotas))
|
||||
"Quota size '%s' is in list of smaller quotas: %s"
|
||||
% (self.size, smaller_quotas)
|
||||
)
|
||||
return True
|
||||
|
||||
if changed_in_period:
|
||||
self.add_note(
|
||||
"Quota has already been updated within the auto "
|
||||
"approve time limit.")
|
||||
"Quota has already been updated within the auto " "approve time limit."
|
||||
)
|
||||
return False
|
||||
|
||||
if self.size not in preapproved_quotas:
|
||||
self.add_note(
|
||||
"Quota size '%s' not in preapproved list: %s" %
|
||||
(self.size, preapproved_quotas))
|
||||
"Quota size '%s' not in preapproved list: %s"
|
||||
% (self.size, preapproved_quotas)
|
||||
)
|
||||
return False
|
||||
|
||||
self.add_note(
|
||||
"Quota size '%s' in preapproved list: %s" %
|
||||
(self.size, preapproved_quotas))
|
||||
"Quota size '%s' in preapproved list: %s" % (self.size, preapproved_quotas)
|
||||
)
|
||||
return True
|
||||
|
||||
def _validate(self):
|
||||
# Make sure the project id is valid and can be used
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_project_id,
|
||||
self._validate_quota_size_exists,
|
||||
self._validate_regions_exist,
|
||||
self._validate_usage_lower_than_quota,
|
||||
])
|
||||
self.action.valid = validate_steps(
|
||||
[
|
||||
self._validate_project_id,
|
||||
self._validate_quota_size_exists,
|
||||
self._validate_regions_exist,
|
||||
self._validate_usage_lower_than_quota,
|
||||
]
|
||||
)
|
||||
self.action.save()
|
||||
|
||||
def _prepare(self):
|
||||
@ -420,8 +422,8 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
||||
self._set_region_quota(region, self.size)
|
||||
|
||||
self.action.state = "completed"
|
||||
self.action.task.cache['project_id'] = self.project_id
|
||||
self.action.task.cache['size'] = self.size
|
||||
self.action.task.cache["project_id"] = self.project_id
|
||||
self.action.task.cache["size"] = self.size
|
||||
|
||||
self.action.save()
|
||||
|
||||
@ -434,6 +436,7 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
|
||||
|
||||
class SetProjectQuotaAction(UpdateProjectQuotasAction):
|
||||
""" Updates quota for a given project to a configured quota level """
|
||||
|
||||
required = []
|
||||
|
||||
serializer = serializers.SetProjectQuotaSerializer
|
||||
@ -454,9 +457,7 @@ class SetProjectQuotaAction(UpdateProjectQuotasAction):
|
||||
|
||||
def _validate(self):
|
||||
# Make sure the project id is valid and can be used
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_project_id,
|
||||
])
|
||||
self.action.valid = validate_steps([self._validate_project_id,])
|
||||
self.action.save()
|
||||
|
||||
def _prepare(self):
|
||||
@ -466,7 +467,7 @@ class SetProjectQuotaAction(UpdateProjectQuotasAction):
|
||||
|
||||
def _approve(self):
|
||||
# Assumption: another action has placed the project_id into the cache.
|
||||
self.project_id = self.action.task.cache.get('project_id', None)
|
||||
self.project_id = self.action.task.cache.get("project_id", None)
|
||||
self._validate()
|
||||
|
||||
if not self.valid or self.action.state == "completed":
|
||||
|
@ -32,7 +32,8 @@ class BaseUserNameSerializer(serializers.Serializer):
|
||||
"""
|
||||
A serializer where the user is identified by username/email.
|
||||
"""
|
||||
domain_id = serializers.CharField(max_length=64, default='default')
|
||||
|
||||
domain_id = serializers.CharField(max_length=64, default="default")
|
||||
username = serializers.CharField(max_length=255)
|
||||
email = serializers.EmailField()
|
||||
|
||||
@ -40,7 +41,7 @@ class BaseUserNameSerializer(serializers.Serializer):
|
||||
super(BaseUserNameSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
if CONF.identity.username_is_email:
|
||||
self.fields.pop('username')
|
||||
self.fields.pop("username")
|
||||
|
||||
|
||||
class BaseUserIdSerializer(serializers.Serializer):
|
||||
@ -53,35 +54,36 @@ class NewUserSerializer(BaseUserNameSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NewUserSerializer, self).__init__(*args, **kwargs)
|
||||
# NOTE(adriant): This overide is mostly in use so that it can be tested
|
||||
self.fields['roles'] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set)
|
||||
self.fields['inherited_roles'] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set)
|
||||
self.fields["roles"] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set
|
||||
)
|
||||
self.fields["inherited_roles"] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
if not data['roles'] and not data['inherited_roles']:
|
||||
if not data["roles"] and not data["inherited_roles"]:
|
||||
raise serializers.ValidationError(
|
||||
"Must supply either 'roles' or 'inherited_roles', or both.")
|
||||
"Must supply either 'roles' or 'inherited_roles', or both."
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class NewProjectSerializer(serializers.Serializer):
|
||||
parent_id = serializers.CharField(
|
||||
max_length=64, default=None, allow_null=True)
|
||||
parent_id = serializers.CharField(max_length=64, default=None, allow_null=True)
|
||||
project_name = serializers.CharField(max_length=64)
|
||||
domain_id = serializers.CharField(max_length=64, default='default')
|
||||
domain_id = serializers.CharField(max_length=64, default="default")
|
||||
description = serializers.CharField(default="", allow_blank=True)
|
||||
|
||||
|
||||
class NewProjectWithUserSerializer(BaseUserNameSerializer):
|
||||
parent_id = serializers.CharField(
|
||||
max_length=64, default=None, allow_null=True)
|
||||
parent_id = serializers.CharField(max_length=64, default=None, allow_null=True)
|
||||
project_name = serializers.CharField(max_length=64)
|
||||
|
||||
|
||||
class ResetUserPasswordSerializer(BaseUserNameSerializer):
|
||||
domain_name = serializers.CharField(max_length=64, default='Default')
|
||||
domain_name = serializers.CharField(max_length=64, default="Default")
|
||||
# override domain_id so serializer doesn't set it up.
|
||||
domain_id = None
|
||||
|
||||
@ -93,15 +95,18 @@ class EditUserRolesSerializer(BaseUserIdSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EditUserRolesSerializer, self).__init__(*args, **kwargs)
|
||||
# NOTE(adriant): This overide is mostly in use so that it can be tested
|
||||
self.fields['roles'] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set)
|
||||
self.fields['inherited_roles'] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set)
|
||||
self.fields["roles"] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set
|
||||
)
|
||||
self.fields["inherited_roles"] = serializers.MultipleChoiceField(
|
||||
choices=get_role_choices(), default=set
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
if not data['roles'] and not data['inherited_roles']:
|
||||
if not data["roles"] and not data["inherited_roles"]:
|
||||
raise serializers.ValidationError(
|
||||
"Must supply either 'roles' or 'inherited_roles', or both.")
|
||||
"Must supply either 'roles' or 'inherited_roles', or both."
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@ -118,7 +123,7 @@ class NewProjectDefaultNetworkSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class AddDefaultUsersToProjectSerializer(serializers.Serializer):
|
||||
domain_id = serializers.CharField(max_length=64, default='default')
|
||||
domain_id = serializers.CharField(max_length=64, default="default")
|
||||
|
||||
|
||||
class SetProjectQuotaSerializer(serializers.Serializer):
|
||||
@ -142,8 +147,9 @@ class UpdateProjectQuotasSerializer(serializers.Serializer):
|
||||
# NOTE(amelia): This overide is mostly in use so that it can be tested
|
||||
# However it does take into account the improbable edge case that the
|
||||
# regions have changed since the server was last started
|
||||
self.fields['regions'] = serializers.MultipleChoiceField(
|
||||
choices=get_region_choices())
|
||||
self.fields["regions"] = serializers.MultipleChoiceField(
|
||||
choices=get_region_choices()
|
||||
)
|
||||
|
||||
def validate_size(self, value):
|
||||
"""
|
||||
@ -151,6 +157,5 @@ class UpdateProjectQuotasSerializer(serializers.Serializer):
|
||||
"""
|
||||
size_list = CONF.quota.sizes.keys()
|
||||
if value not in size_list:
|
||||
raise serializers.ValidationError("Quota size: %s is not valid"
|
||||
% value)
|
||||
raise serializers.ValidationError("Quota size: %s is not valid" % value)
|
||||
return value
|
||||
|
@ -27,11 +27,11 @@ from adjutant.common.tests.utils import AdjutantTestCase
|
||||
from adjutant.config import CONF
|
||||
|
||||
default_email_conf = {
|
||||
'from': "adjutant@example.com",
|
||||
'reply': 'adjutant@example.com',
|
||||
'template': 'initial.txt',
|
||||
'html_template': 'completed.txt',
|
||||
'subject': 'additional email'
|
||||
"from": "adjutant@example.com",
|
||||
"reply": "adjutant@example.com",
|
||||
"template": "initial.txt",
|
||||
"html_template": "completed.txt",
|
||||
"subject": "additional email",
|
||||
}
|
||||
|
||||
|
||||
@ -40,22 +40,15 @@ class FailEmail(mock.MagicMock):
|
||||
raise SMTPException
|
||||
|
||||
|
||||
@mock.patch('adjutant.common.user_store.IdentityManager',
|
||||
FakeManager)
|
||||
@mock.patch("adjutant.common.user_store.IdentityManager", FakeManager)
|
||||
class MiscActionTests(AdjutantTestCase):
|
||||
|
||||
def test_send_email(self):
|
||||
# include html template
|
||||
to_address = "test@example.com"
|
||||
|
||||
task = Task.objects.create(
|
||||
keystone_user={}
|
||||
)
|
||||
task = Task.objects.create(keystone_user={})
|
||||
|
||||
context = {
|
||||
'task': task,
|
||||
'actions': ["action_1", "action_2"]
|
||||
}
|
||||
context = {"task": task, "actions": ["action_1", "action_2"]}
|
||||
|
||||
result = send_email(to_address, context, default_email_conf, task)
|
||||
|
||||
@ -67,58 +60,53 @@ class MiscActionTests(AdjutantTestCase):
|
||||
def test_send_email_no_addresses(self):
|
||||
to_address = []
|
||||
|
||||
task = Task.objects.create(
|
||||
keystone_user={}
|
||||
)
|
||||
task = Task.objects.create(keystone_user={})
|
||||
|
||||
context = {
|
||||
'task': task,
|
||||
'actions': ["action_1", "action_2"]
|
||||
}
|
||||
context = {"task": task, "actions": ["action_1", "action_2"]}
|
||||
|
||||
result = send_email(to_address, context, default_email_conf, task)
|
||||
self.assertEqual(result, None)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
@mock.patch('adjutant.actions.utils.EmailMultiAlternatives',
|
||||
FailEmail)
|
||||
@mock.patch("adjutant.actions.utils.EmailMultiAlternatives", FailEmail)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
|
||||
{'operation': 'overlay', 'value': {
|
||||
'email_task_cache': True,
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}},
|
||||
{
|
||||
"operation": "overlay",
|
||||
"value": {
|
||||
"email_task_cache": True,
|
||||
"subject": "Email Subject",
|
||||
"template": "token.txt",
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
)
|
||||
def test_send_additional_email_fail(self):
|
||||
"""
|
||||
Tests that a failure to send an additional email doesn't cause
|
||||
it to become invalid or break.
|
||||
"""
|
||||
|
||||
task = Task.objects.create(
|
||||
keystone_user={},
|
||||
task_type='edit_roles',
|
||||
)
|
||||
task = Task.objects.create(keystone_user={}, task_type="edit_roles",)
|
||||
|
||||
action = SendAdditionalEmailAction({}, task=task, order=1)
|
||||
|
||||
action.prepare()
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
task.cache["additional_emails"] = ["thisguy@righthere.com",
|
||||
"nope@example.com"]
|
||||
task.cache["additional_emails"] = ["thisguy@righthere.com", "nope@example.com"]
|
||||
|
||||
action.approve()
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
self.assertTrue(
|
||||
"Unable to send additional email. Stage: approve" in
|
||||
action.action.task.action_notes['SendAdditionalEmailAction'][1])
|
||||
"Unable to send additional email. Stage: approve"
|
||||
in action.action.task.action_notes["SendAdditionalEmailAction"][1]
|
||||
)
|
||||
|
||||
action.submit({})
|
||||
self.assertEqual(action.valid, True)
|
||||
@ -127,37 +115,39 @@ class MiscActionTests(AdjutantTestCase):
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
|
||||
{'operation': 'overlay', 'value': {
|
||||
'email_task_cache': True,
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}},
|
||||
{
|
||||
"operation": "overlay",
|
||||
"value": {
|
||||
"email_task_cache": True,
|
||||
"subject": "Email Subject",
|
||||
"template": "token.txt",
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
)
|
||||
def test_send_additional_email_task_cache(self):
|
||||
"""
|
||||
Tests sending an additional email with the address placed in the
|
||||
task cache.
|
||||
"""
|
||||
|
||||
task = Task.objects.create(
|
||||
keystone_user={}
|
||||
)
|
||||
task = Task.objects.create(keystone_user={})
|
||||
|
||||
action = SendAdditionalEmailAction({}, task=task, order=1)
|
||||
|
||||
action.prepare()
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
task.cache["additional_emails"] = ["thisguy@righthere.com",
|
||||
"nope@example.com"]
|
||||
task.cache["additional_emails"] = ["thisguy@righthere.com", "nope@example.com"]
|
||||
|
||||
action.approve()
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(set(mail.outbox[0].to),
|
||||
set(["thisguy@righthere.com", "nope@example.com"]))
|
||||
self.assertEqual(
|
||||
set(mail.outbox[0].to), set(["thisguy@righthere.com", "nope@example.com"])
|
||||
)
|
||||
|
||||
action.submit({})
|
||||
self.assertEqual(action.valid, True)
|
||||
@ -167,22 +157,24 @@ class MiscActionTests(AdjutantTestCase):
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
|
||||
{'operation': 'overlay', 'value': {
|
||||
'email_task_cache': True,
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}},
|
||||
{
|
||||
"operation": "overlay",
|
||||
"value": {
|
||||
"email_task_cache": True,
|
||||
"subject": "Email Subject",
|
||||
"template": "token.txt",
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
)
|
||||
def test_send_additional_email_task_cache_none_set(self):
|
||||
"""
|
||||
Tests sending an additional email with 'email_task_cache' set but
|
||||
no address placed in the task cache.
|
||||
"""
|
||||
|
||||
task = Task.objects.create(
|
||||
keystone_user={}
|
||||
)
|
||||
task = Task.objects.create(keystone_user={})
|
||||
|
||||
action = SendAdditionalEmailAction({}, task=task, order=1)
|
||||
|
||||
@ -201,23 +193,24 @@ class MiscActionTests(AdjutantTestCase):
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
|
||||
{'operation': 'overlay', 'value': {
|
||||
'email_additional_addresses': [
|
||||
'anadminwhocares@example.com'],
|
||||
'subject': 'Email Subject',
|
||||
'template': 'token.txt'
|
||||
}},
|
||||
{
|
||||
"operation": "overlay",
|
||||
"value": {
|
||||
"email_additional_addresses": ["anadminwhocares@example.com"],
|
||||
"subject": "Email Subject",
|
||||
"template": "token.txt",
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
)
|
||||
def test_send_additional_email_email_in_config(self):
|
||||
"""
|
||||
Tests sending an additional email with the address placed in the
|
||||
task cache.
|
||||
"""
|
||||
|
||||
task = Task.objects.create(
|
||||
keystone_user={}
|
||||
)
|
||||
task = Task.objects.create(keystone_user={})
|
||||
|
||||
action = SendAdditionalEmailAction({}, task=task, order=1)
|
||||
|
||||
@ -228,8 +221,7 @@ class MiscActionTests(AdjutantTestCase):
|
||||
self.assertEqual(action.valid, True)
|
||||
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].to,
|
||||
["anadminwhocares@example.com"])
|
||||
self.assertEqual(mail.outbox[0].to, ["anadminwhocares@example.com"])
|
||||
|
||||
action.submit({})
|
||||
self.assertEqual(action.valid, True)
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,11 @@ from confspirator import fields
|
||||
from adjutant.config import CONF
|
||||
from adjutant.common import user_store
|
||||
from adjutant.actions.v1.base import (
|
||||
UserNameAction, UserIdAction, UserMixin, ProjectMixin)
|
||||
UserNameAction,
|
||||
UserIdAction,
|
||||
UserMixin,
|
||||
ProjectMixin,
|
||||
)
|
||||
from adjutant.actions.v1 import serializers
|
||||
from adjutant.actions.utils import validate_steps
|
||||
|
||||
@ -32,12 +36,12 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
"""
|
||||
|
||||
required = [
|
||||
'username',
|
||||
'email',
|
||||
'project_id',
|
||||
'roles',
|
||||
'inherited_roles',
|
||||
'domain_id',
|
||||
"username",
|
||||
"email",
|
||||
"project_id",
|
||||
"roles",
|
||||
"inherited_roles",
|
||||
"domain_id",
|
||||
]
|
||||
|
||||
serializer = serializers.NewUserSerializer
|
||||
@ -51,37 +55,43 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
if not user:
|
||||
self.add_note(
|
||||
"No user present with username '%s'. "
|
||||
"Need to create new user." % self.username)
|
||||
"Need to create new user." % self.username
|
||||
)
|
||||
if not id_manager.can_edit_users:
|
||||
self.add_note(
|
||||
'Identity backend does not support user editing, '
|
||||
'cannot create new user.')
|
||||
"Identity backend does not support user editing, "
|
||||
"cannot create new user."
|
||||
)
|
||||
return False
|
||||
self.action.need_token = True
|
||||
# add to cache to use in template
|
||||
self.action.task.cache['user_state'] = "default"
|
||||
self.action.task.cache["user_state"] = "default"
|
||||
self.set_token_fields(["password"])
|
||||
return True
|
||||
if (not CONF.identity.username_is_email
|
||||
and getattr(user, 'email', None) != self.email):
|
||||
if (
|
||||
not CONF.identity.username_is_email
|
||||
and getattr(user, "email", None) != self.email
|
||||
):
|
||||
self.add_note(
|
||||
'Found matching username, but email did not match. '
|
||||
'Reporting as invalid.')
|
||||
"Found matching username, but email did not match. "
|
||||
"Reporting as invalid."
|
||||
)
|
||||
return False
|
||||
|
||||
if not user.enabled:
|
||||
self.add_note(
|
||||
"Existing disabled user '%s' with matching email." %
|
||||
self.email)
|
||||
"Existing disabled user '%s' with matching email." % self.email
|
||||
)
|
||||
if not id_manager.can_edit_users:
|
||||
self.add_note(
|
||||
'Identity backend does not support user editing, '
|
||||
'cannot renable user.')
|
||||
"Identity backend does not support user editing, "
|
||||
"cannot renable user."
|
||||
)
|
||||
return False
|
||||
self.action.need_token = True
|
||||
self.action.state = "disabled"
|
||||
# add to cache to use in template
|
||||
self.action.task.cache['user_state'] = "disabled"
|
||||
self.action.task.cache["user_state"] = "disabled"
|
||||
# as they are disabled we'll reset their password
|
||||
self.set_token_fields(["password"])
|
||||
return True
|
||||
@ -93,30 +103,29 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
if not missing:
|
||||
self.action.need_token = False
|
||||
self.action.state = "complete"
|
||||
self.add_note(
|
||||
'Existing user already has roles.'
|
||||
)
|
||||
self.add_note("Existing user already has roles.")
|
||||
else:
|
||||
self.roles = list(missing)
|
||||
self.action.need_token = True
|
||||
self.set_token_fields(["confirm"])
|
||||
self.action.state = "existing"
|
||||
# add to cache to use in template
|
||||
self.action.task.cache['user_state'] = "existing"
|
||||
self.add_note(
|
||||
'Existing user with matching email missing roles.')
|
||||
self.action.task.cache["user_state"] = "existing"
|
||||
self.add_note("Existing user with matching email missing roles.")
|
||||
|
||||
return True
|
||||
|
||||
def _validate(self):
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_role_permissions,
|
||||
self._validate_keystone_user_domain_id,
|
||||
self._validate_keystone_user_project_id,
|
||||
self._validate_domain_id,
|
||||
self._validate_project_id,
|
||||
self._validate_target_user,
|
||||
])
|
||||
self.action.valid = validate_steps(
|
||||
[
|
||||
self._validate_role_permissions,
|
||||
self._validate_keystone_user_domain_id,
|
||||
self._validate_keystone_user_project_id,
|
||||
self._validate_domain_id,
|
||||
self._validate_project_id,
|
||||
self._validate_target_user,
|
||||
]
|
||||
)
|
||||
self.action.save()
|
||||
|
||||
def _prepare(self):
|
||||
@ -134,13 +143,14 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
|
||||
if self.action.state == "default":
|
||||
# default action: Create a new user in the tenant and add roles
|
||||
user = self.create_user(token_data['password'])
|
||||
user = self.create_user(token_data["password"])
|
||||
self.grant_roles(user, self.roles, self.project_id)
|
||||
self.grant_roles(user, self.inherited_roles, self.project_id, True)
|
||||
|
||||
self.add_note(
|
||||
'User %s has been created, with roles %s in project %s.'
|
||||
% (self.username, self.roles, self.project_id))
|
||||
"User %s has been created, with roles %s in project %s."
|
||||
% (self.username, self.roles, self.project_id)
|
||||
)
|
||||
|
||||
elif self.action.state == "disabled":
|
||||
# first re-enable user
|
||||
@ -148,14 +158,14 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
self.enable_user(user)
|
||||
self.grant_roles(user, self.roles, self.project_id)
|
||||
self.grant_roles(user, self.inherited_roles, self.project_id, True)
|
||||
self.update_password(token_data['password'])
|
||||
self.update_password(token_data["password"])
|
||||
|
||||
self.add_note('User %s password has been changed.' % self.username)
|
||||
self.add_note("User %s password has been changed." % self.username)
|
||||
|
||||
self.add_note(
|
||||
'Existing user %s has been re-enabled and given roles %s'
|
||||
' in project %s.'
|
||||
% (self.username, self.roles, self.project_id))
|
||||
"Existing user %s has been re-enabled and given roles %s"
|
||||
" in project %s." % (self.username, self.roles, self.project_id)
|
||||
)
|
||||
|
||||
elif self.action.state == "existing":
|
||||
# Existing action: only add roles.
|
||||
@ -164,13 +174,15 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
|
||||
self.grant_roles(user, self.inherited_roles, self.project_id, True)
|
||||
|
||||
self.add_note(
|
||||
'Existing user %s has been given roles %s in project %s.'
|
||||
% (self.username, self.roles, self.project_id))
|
||||
"Existing user %s has been given roles %s in project %s."
|
||||
% (self.username, self.roles, self.project_id)
|
||||
)
|
||||
elif self.action.state == "complete":
|
||||
# complete action: nothing to do.
|
||||
self.add_note(
|
||||
'Existing user %s already had roles %s in project %s.'
|
||||
% (self.username, self.roles, self.project_id))
|
||||
"Existing user %s already had roles %s in project %s."
|
||||
% (self.username, self.roles, self.project_id)
|
||||
)
|
||||
|
||||
|
||||
class ResetUserPasswordAction(UserNameAction, UserMixin):
|
||||
@ -178,11 +190,7 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
|
||||
Simple action to reset a password for a given user.
|
||||
"""
|
||||
|
||||
required = [
|
||||
'domain_name',
|
||||
'username',
|
||||
'email'
|
||||
]
|
||||
required = ["domain_name", "username", "email"]
|
||||
|
||||
serializer = serializers.ResetUserPasswordSerializer
|
||||
|
||||
@ -192,7 +200,7 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
|
||||
"blacklisted_roles",
|
||||
help_text="Users with these roles cannot reset their passwords.",
|
||||
default=[],
|
||||
sample_default=['admin'],
|
||||
sample_default=["admin"],
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -210,7 +218,7 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
|
||||
user_roles.extend(role.name for role in roles)
|
||||
|
||||
if set(self.config.blacklisted_roles) & set(user_roles):
|
||||
self.add_note('Cannot reset users with blacklisted roles.')
|
||||
self.add_note("Cannot reset users with blacklisted roles.")
|
||||
return False
|
||||
|
||||
return True
|
||||
@ -219,26 +227,28 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
|
||||
# NOTE(adriant): We only need to check the USERNAME_IS_EMAIL=False
|
||||
# case since '_validate_username_exists' will ensure the True case
|
||||
if not CONF.identity.username_is_email:
|
||||
if (self.user and (
|
||||
getattr(self.user, 'email', None).lower()
|
||||
!= self.email.lower())):
|
||||
self.add_note('Existing user with non-matching email.')
|
||||
if self.user and (
|
||||
getattr(self.user, "email", None).lower() != self.email.lower()
|
||||
):
|
||||
self.add_note("Existing user with non-matching email.")
|
||||
return False
|
||||
|
||||
self.action.need_token = True
|
||||
self.set_token_fields(["password"])
|
||||
self.add_note('Existing user with matching email.')
|
||||
self.add_note("Existing user with matching email.")
|
||||
return True
|
||||
|
||||
def _validate(self):
|
||||
# Here, the order of validation matters
|
||||
# as each one adds new class variables
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_domain_name,
|
||||
self._validate_username_exists,
|
||||
self._validate_user_roles,
|
||||
self._validate_user_email,
|
||||
])
|
||||
self.action.valid = validate_steps(
|
||||
[
|
||||
self._validate_domain_name,
|
||||
self._validate_username_exists,
|
||||
self._validate_user_roles,
|
||||
self._validate_user_email,
|
||||
]
|
||||
)
|
||||
self.action.save()
|
||||
|
||||
def _prepare(self):
|
||||
@ -254,8 +264,8 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
|
||||
if not self.valid:
|
||||
return
|
||||
|
||||
self.update_password(token_data['password'])
|
||||
self.add_note('User %s password has been changed.' % self.username)
|
||||
self.update_password(token_data["password"])
|
||||
self.add_note("User %s password has been changed." % self.username)
|
||||
|
||||
|
||||
class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
|
||||
@ -264,13 +274,7 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
|
||||
on a user for the given project.
|
||||
"""
|
||||
|
||||
required = [
|
||||
'project_id',
|
||||
'user_id',
|
||||
'roles',
|
||||
'inherited_roles',
|
||||
'remove'
|
||||
]
|
||||
required = ["project_id", "user_id", "roles", "inherited_roles", "remove"]
|
||||
|
||||
serializer = serializers.EditUserRolesSerializer
|
||||
|
||||
@ -278,7 +282,7 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
|
||||
# Get target user
|
||||
user = self._get_target_user()
|
||||
if not user:
|
||||
self.add_note('No user present with user_id')
|
||||
self.add_note("No user present with user_id")
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -288,37 +292,31 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
|
||||
project = id_manager.get_project(self.project_id)
|
||||
# user roles
|
||||
current_roles = id_manager.get_roles(user, project)
|
||||
current_inherited_roles = id_manager.get_roles(
|
||||
user, project, inherited=True)
|
||||
current_inherited_roles = id_manager.get_roles(user, project, inherited=True)
|
||||
current_roles = {role.name for role in current_roles}
|
||||
current_inherited_roles = {
|
||||
role.name for role in current_inherited_roles}
|
||||
current_inherited_roles = {role.name for role in current_inherited_roles}
|
||||
if self.remove:
|
||||
remaining = set(current_roles) & set(self.roles)
|
||||
remaining_inherited = (
|
||||
set(current_inherited_roles) & set(self.inherited_roles))
|
||||
remaining_inherited = set(current_inherited_roles) & set(
|
||||
self.inherited_roles
|
||||
)
|
||||
if not remaining and not remaining_inherited:
|
||||
self.action.state = "complete"
|
||||
self.add_note(
|
||||
"User doesn't have roles to remove.")
|
||||
self.add_note("User doesn't have roles to remove.")
|
||||
else:
|
||||
self.roles = list(remaining)
|
||||
self.inherited_roles = list(remaining_inherited)
|
||||
self.add_note(
|
||||
'User has roles to remove.')
|
||||
self.add_note("User has roles to remove.")
|
||||
else:
|
||||
missing = set(self.roles) - set(current_roles)
|
||||
missing_inherited = (
|
||||
set(self.inherited_roles) - set(current_inherited_roles))
|
||||
missing_inherited = set(self.inherited_roles) - set(current_inherited_roles)
|
||||
if not missing and not missing_inherited:
|
||||
self.action.state = "complete"
|
||||
self.add_note(
|
||||
'User already has roles.')
|
||||
self.add_note("User already has roles.")
|
||||
else:
|
||||
self.roles = list(missing)
|
||||
self.inherited_roles = list(missing_inherited)
|
||||
self.add_note(
|
||||
'User missing roles.')
|
||||
self.add_note("User missing roles.")
|
||||
# All paths are valid here
|
||||
# We've just set state and roles that need to be changed.
|
||||
return True
|
||||
@ -327,18 +325,21 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
|
||||
|
||||
id_manager = user_store.IdentityManager()
|
||||
|
||||
current_user_roles = id_manager.get_roles(project=self.project_id,
|
||||
user=self.user_id)
|
||||
current_user_roles = id_manager.get_roles(
|
||||
project=self.project_id, user=self.user_id
|
||||
)
|
||||
current_user_roles = [role.name for role in current_user_roles]
|
||||
|
||||
current_roles_manageable = self.are_roles_manageable(
|
||||
self.action.task.keystone_user['roles'], current_user_roles)
|
||||
self.action.task.keystone_user["roles"], current_user_roles
|
||||
)
|
||||
|
||||
all_roles = set()
|
||||
all_roles.update(self.roles)
|
||||
all_roles.update(self.inherited_roles)
|
||||
new_roles_manageable = self.are_roles_manageable(
|
||||
self.action.task.keystone_user['roles'], all_roles)
|
||||
self.action.task.keystone_user["roles"], all_roles
|
||||
)
|
||||
|
||||
if new_roles_manageable and current_roles_manageable:
|
||||
self.add_note("All user roles are manageable.")
|
||||
@ -347,13 +348,15 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
|
||||
return False
|
||||
|
||||
def _validate(self):
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_keystone_user_project_id,
|
||||
self._validate_role_permissions,
|
||||
self._validate_project_id,
|
||||
self._validate_target_user,
|
||||
self._validate_user_roles,
|
||||
])
|
||||
self.action.valid = validate_steps(
|
||||
[
|
||||
self._validate_keystone_user_project_id,
|
||||
self._validate_role_permissions,
|
||||
self._validate_project_id,
|
||||
self._validate_target_user,
|
||||
self._validate_user_roles,
|
||||
]
|
||||
)
|
||||
self.action.save()
|
||||
|
||||
def _prepare(self):
|
||||
@ -371,37 +374,47 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
|
||||
|
||||
if self.action.state == "default":
|
||||
user = self._get_target_user()
|
||||
self._user_roles_edit(user, self.roles, self.project_id,
|
||||
remove=self.remove)
|
||||
self._user_roles_edit(user, self.inherited_roles, self.project_id,
|
||||
remove=self.remove, inherited=True)
|
||||
self._user_roles_edit(user, self.roles, self.project_id, remove=self.remove)
|
||||
self._user_roles_edit(
|
||||
user,
|
||||
self.inherited_roles,
|
||||
self.project_id,
|
||||
remove=self.remove,
|
||||
inherited=True,
|
||||
)
|
||||
|
||||
if self.remove and self.roles:
|
||||
self.add_note(
|
||||
'User %s has had roles %s removed from project %s.'
|
||||
% (self.user_id, self.roles, self.project_id))
|
||||
"User %s has had roles %s removed from project %s."
|
||||
% (self.user_id, self.roles, self.project_id)
|
||||
)
|
||||
if self.remove and self.inherited_roles:
|
||||
self.add_note(
|
||||
'User %s has had inherited roles %s '
|
||||
'removed from project %s.'
|
||||
% (self.user_id, self.inherited_roles, self.project_id))
|
||||
"User %s has had inherited roles %s "
|
||||
"removed from project %s."
|
||||
% (self.user_id, self.inherited_roles, self.project_id)
|
||||
)
|
||||
if self.roles:
|
||||
self.add_note(
|
||||
'User %s has been given roles %s in project %s.'
|
||||
% (self.user_id, self.roles, self.project_id))
|
||||
"User %s has been given roles %s in project %s."
|
||||
% (self.user_id, self.roles, self.project_id)
|
||||
)
|
||||
if self.inherited_roles:
|
||||
self.add_note(
|
||||
'User %s has been given inherited roles %s in project %s.'
|
||||
% (self.user_id, self.inherited_roles, self.project_id))
|
||||
"User %s has been given inherited roles %s in project %s."
|
||||
% (self.user_id, self.inherited_roles, self.project_id)
|
||||
)
|
||||
elif self.action.state == "complete":
|
||||
if self.remove:
|
||||
self.add_note(
|
||||
"User %s didn't have roles %s in project %s."
|
||||
% (self.user_id, self.roles, self.project_id))
|
||||
% (self.user_id, self.roles, self.project_id)
|
||||
)
|
||||
else:
|
||||
self.add_note(
|
||||
'User %s already had roles %s in project %s.'
|
||||
% (self.user_id, self.roles, self.project_id))
|
||||
"User %s already had roles %s in project %s."
|
||||
% (self.user_id, self.roles, self.project_id)
|
||||
)
|
||||
|
||||
|
||||
class UpdateUserEmailAction(UserIdAction, UserMixin):
|
||||
@ -410,8 +423,8 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
|
||||
"""
|
||||
|
||||
required = [
|
||||
'user_id',
|
||||
'new_email',
|
||||
"user_id",
|
||||
"new_email",
|
||||
]
|
||||
|
||||
serializer = serializers.UpdateUserEmailSerializer
|
||||
@ -421,10 +434,9 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
|
||||
return self.new_email
|
||||
|
||||
def _validate(self):
|
||||
self.action.valid = validate_steps([
|
||||
self._validate_user,
|
||||
self._validate_email_not_in_use,
|
||||
])
|
||||
self.action.valid = validate_steps(
|
||||
[self._validate_user, self._validate_email_not_in_use,]
|
||||
)
|
||||
self.action.save()
|
||||
|
||||
def _validate_user(self):
|
||||
@ -436,8 +448,7 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
|
||||
|
||||
def _validate_email_not_in_use(self):
|
||||
if CONF.identity.username_is_email:
|
||||
self.domain_id = self.action.task.keystone_user[
|
||||
'project_domain_id']
|
||||
self.domain_id = self.action.task.keystone_user["project_domain_id"]
|
||||
|
||||
id_manager = user_store.IdentityManager()
|
||||
|
||||
@ -469,5 +480,7 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
|
||||
if CONF.identity.username_is_email:
|
||||
self.update_user_name(self.new_email, user=self.user)
|
||||
|
||||
self.add_note('The email for user %s has been changed to %s.'
|
||||
% (self.old_username, self.new_email))
|
||||
self.add_note(
|
||||
"The email for user %s has been changed to %s."
|
||||
% (self.old_username, self.new_email)
|
||||
)
|
||||
|
@ -23,7 +23,7 @@ from adjutant import exceptions
|
||||
from adjutant.notifications.utils import create_notification
|
||||
|
||||
|
||||
LOG = getLogger('adjutant')
|
||||
LOG = getLogger("adjutant")
|
||||
|
||||
|
||||
def exception_handler(exc, context):
|
||||
@ -38,17 +38,17 @@ def exception_handler(exc, context):
|
||||
|
||||
if isinstance(exc, exceptions.BaseAPIException):
|
||||
if isinstance(exc.message, (list, dict)):
|
||||
data = {'errors': exc.message}
|
||||
data = {"errors": exc.message}
|
||||
else:
|
||||
data = {'errors': [exc.message]}
|
||||
data = {"errors": [exc.message]}
|
||||
note_data = data
|
||||
|
||||
if isinstance(exc, exceptions.TaskActionsFailed):
|
||||
if exc.internal_message:
|
||||
if isinstance(exc.internal_message, (list, dict)):
|
||||
note_data = {'errors': exc.internal_message}
|
||||
note_data = {"errors": exc.internal_message}
|
||||
else:
|
||||
note_data = {'errors': [exc.internal_message]}
|
||||
note_data = {"errors": [exc.internal_message]}
|
||||
create_notification(exc.task, note_data, error=True)
|
||||
|
||||
LOG.info("(%s) - %s" % (now, exc))
|
||||
|
@ -9,50 +9,78 @@ import adjutant.api.models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
name="Notification",
|
||||
fields=[
|
||||
('uuid', models.CharField(default=adjutant.api.models.hex_uuid, max_length=32, serialize=False, 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)),
|
||||
(
|
||||
"uuid",
|
||||
models.CharField(
|
||||
default=adjutant.api.models.hex_uuid,
|
||||
max_length=32,
|
||||
serialize=False,
|
||||
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)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Task',
|
||||
name="Task",
|
||||
fields=[
|
||||
('uuid', models.CharField(default=adjutant.api.models.hex_uuid, max_length=32, serialize=False, primary_key=True)),
|
||||
('hash_key', models.CharField(max_length=32, db_index=True)),
|
||||
('ip_address', models.GenericIPAddressField()),
|
||||
('keystone_user', jsonfield.fields.JSONField(default={})),
|
||||
('project_id', models.CharField(max_length=32, null=True, db_index=True)),
|
||||
('task_type', models.CharField(max_length=100, 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)),
|
||||
('completed', models.BooleanField(default=False, db_index=True)),
|
||||
('created_on', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('approved_on', models.DateTimeField(null=True)),
|
||||
('completed_on', models.DateTimeField(null=True)),
|
||||
(
|
||||
"uuid",
|
||||
models.CharField(
|
||||
default=adjutant.api.models.hex_uuid,
|
||||
max_length=32,
|
||||
serialize=False,
|
||||
primary_key=True,
|
||||
),
|
||||
),
|
||||
("hash_key", models.CharField(max_length=32, db_index=True)),
|
||||
("ip_address", models.GenericIPAddressField()),
|
||||
("keystone_user", jsonfield.fields.JSONField(default={})),
|
||||
(
|
||||
"project_id",
|
||||
models.CharField(max_length=32, null=True, db_index=True),
|
||||
),
|
||||
("task_type", models.CharField(max_length=100, 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)),
|
||||
("completed", models.BooleanField(default=False, db_index=True)),
|
||||
("created_on", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("approved_on", models.DateTimeField(null=True)),
|
||||
("completed_on", models.DateTimeField(null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Token',
|
||||
name="Token",
|
||||
fields=[
|
||||
('token', models.CharField(max_length=32, serialize=False, primary_key=True)),
|
||||
('created_on', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('expires', models.DateTimeField(db_index=True)),
|
||||
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Task')),
|
||||
(
|
||||
"token",
|
||||
models.CharField(max_length=32, serialize=False, primary_key=True),
|
||||
),
|
||||
("created_on", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("expires", models.DateTimeField(db_index=True)),
|
||||
(
|
||||
"task",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.Task"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notification',
|
||||
name='task',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Task'),
|
||||
model_name="notification",
|
||||
name="task",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.Task"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -7,13 +7,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0001_initial'),
|
||||
("api", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='hash_key',
|
||||
model_name="task",
|
||||
name="hash_key",
|
||||
field=models.CharField(max_length=64, db_index=True),
|
||||
),
|
||||
]
|
||||
|
@ -8,13 +8,13 @@ import jsonfield.fields
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0002_auto_20160815_2249'),
|
||||
("api", "0002_auto_20160815_2249"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='approved_by',
|
||||
model_name="task",
|
||||
name="approved_by",
|
||||
field=jsonfield.fields.JSONField(default={}),
|
||||
),
|
||||
]
|
||||
|
@ -7,13 +7,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0003_task_approved_by'),
|
||||
("api", "0003_task_approved_by"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='project_id',
|
||||
model_name="task",
|
||||
name="project_id",
|
||||
field=models.CharField(max_length=64, null=True, db_index=True),
|
||||
),
|
||||
]
|
||||
|
@ -10,16 +10,13 @@ class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('api', '0004_auto_20160929_0317'),
|
||||
("api", "0004_auto_20160929_0317"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
database_operations=[
|
||||
migrations.AlterModelTable(
|
||||
name='task',
|
||||
table='tasks_task',
|
||||
),
|
||||
migrations.AlterModelTable(name="task", table="tasks_task",),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
@ -9,14 +9,16 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tasks', '0001_initial'),
|
||||
('actions', '0003_auto_20190610_0205'),
|
||||
("tasks", "0001_initial"),
|
||||
("actions", "0003_auto_20190610_0205"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='token',
|
||||
name='task',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.Task'),
|
||||
model_name="token",
|
||||
name="task",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="tasks.Task"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -9,14 +9,16 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tasks', '0001_initial'),
|
||||
('actions', '0003_auto_20190610_0205'),
|
||||
("tasks", "0001_initial"),
|
||||
("actions", "0003_auto_20190610_0205"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='task',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.Task'),
|
||||
model_name="notification",
|
||||
name="task",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="tasks.Task"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -8,19 +8,15 @@ from django.db import migrations
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0005_auto_20190610_0209'),
|
||||
('tasks', '0001_initial'),
|
||||
('actions', '0004_auto_20190610_0209'),
|
||||
('api', '0006_auto_20190610_0209'),
|
||||
('api', '0007_auto_20190610_0209'),
|
||||
("api", "0005_auto_20190610_0209"),
|
||||
("tasks", "0001_initial"),
|
||||
("actions", "0004_auto_20190610_0209"),
|
||||
("api", "0006_auto_20190610_0209"),
|
||||
("api", "0007_auto_20190610_0209"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.DeleteModel(
|
||||
name='Task',
|
||||
),
|
||||
],
|
||||
state_operations=[migrations.DeleteModel(name="Task",),],
|
||||
),
|
||||
]
|
||||
|
@ -40,7 +40,7 @@ class Token(models.Model):
|
||||
"task_type": self.task.task_type,
|
||||
"token": self.token,
|
||||
"created_on": self.created_on,
|
||||
"expires": self.expires
|
||||
"expires": self.expires,
|
||||
}
|
||||
|
||||
@property
|
||||
@ -53,8 +53,7 @@ class Notification(models.Model):
|
||||
Notification linked to a task with some notes.
|
||||
"""
|
||||
|
||||
uuid = models.CharField(max_length=32, default=hex_uuid,
|
||||
primary_key=True)
|
||||
uuid = models.CharField(max_length=32, default=hex_uuid, primary_key=True)
|
||||
notes = JSONField(default={})
|
||||
task = models.ForeignKey(Task, on_delete=models.CASCADE)
|
||||
error = models.BooleanField(default=False, db_index=True)
|
||||
@ -68,5 +67,5 @@ class Notification(models.Model):
|
||||
"task": self.task.uuid,
|
||||
"error": self.error,
|
||||
"acknowledged": self.acknowledged,
|
||||
"created_on": self.created_on
|
||||
"created_on": self.created_on,
|
||||
}
|
||||
|
@ -22,15 +22,15 @@ from adjutant.api.views import build_version_details
|
||||
from adjutant.api.v1 import views as views_v1
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.VersionView.as_view()),
|
||||
url(r"^$", views.VersionView.as_view()),
|
||||
]
|
||||
|
||||
# NOTE(adriant): make this conditional once we have a v2.
|
||||
build_version_details('1.0', 'CURRENT', relative_endpoint='v1/')
|
||||
urlpatterns.append(url(r'^v1/?$', views_v1.V1VersionEndpoint.as_view()))
|
||||
urlpatterns.append(url(r'^v1/', include('adjutant.api.v1.urls')))
|
||||
build_version_details("1.0", "CURRENT", relative_endpoint="v1/")
|
||||
urlpatterns.append(url(r"^v1/?$", views_v1.V1VersionEndpoint.as_view()))
|
||||
urlpatterns.append(url(r"^v1/", include("adjutant.api.v1.urls")))
|
||||
|
||||
|
||||
if settings.DEBUG:
|
||||
schema_view = get_swagger_view(title='Adjutant API')
|
||||
urlpatterns.append(url(r'^docs/', schema_view))
|
||||
schema_view = get_swagger_view(title="Adjutant API")
|
||||
urlpatterns.append(url(r"^docs/", schema_view))
|
||||
|
@ -27,17 +27,17 @@ def require_roles(roles, func, *args, **kwargs):
|
||||
"""
|
||||
request = args[1]
|
||||
req_roles = set(roles)
|
||||
if not request.keystone_user.get('authenticated', False):
|
||||
return Response({'errors': ["Credentials incorrect or none given."]},
|
||||
401)
|
||||
if not request.keystone_user.get("authenticated", False):
|
||||
return Response({"errors": ["Credentials incorrect or none given."]}, 401)
|
||||
|
||||
roles = set(request.keystone_user.get('roles', []))
|
||||
roles = set(request.keystone_user.get("roles", []))
|
||||
|
||||
if roles & req_roles:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return Response({'errors': ["Must have one of the following roles: %s" %
|
||||
list(req_roles)]}, 403)
|
||||
return Response(
|
||||
{"errors": ["Must have one of the following roles: %s" % list(req_roles)]}, 403
|
||||
)
|
||||
|
||||
|
||||
@decorator
|
||||
@ -47,7 +47,8 @@ def mod_or_admin(func, *args, **kwargs):
|
||||
Admin is allowed everything, so is also included.
|
||||
"""
|
||||
return require_roles(
|
||||
{'project_admin', 'project_mod', 'admin'}, func, *args, **kwargs)
|
||||
{"project_admin", "project_mod", "admin"}, func, *args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
@decorator
|
||||
@ -55,8 +56,7 @@ def project_admin(func, *args, **kwargs):
|
||||
"""
|
||||
endpoints setup with this decorator require the admin/project admin role.
|
||||
"""
|
||||
return require_roles(
|
||||
{'project_admin', 'admin'}, func, *args, **kwargs)
|
||||
return require_roles({"project_admin", "admin"}, func, *args, **kwargs)
|
||||
|
||||
|
||||
@decorator
|
||||
@ -64,8 +64,7 @@ def admin(func, *args, **kwargs):
|
||||
"""
|
||||
endpoints setup with this decorator require the admin role.
|
||||
"""
|
||||
return require_roles(
|
||||
{'admin'}, func, *args, **kwargs)
|
||||
return require_roles({"admin"}, func, *args, **kwargs)
|
||||
|
||||
|
||||
@decorator
|
||||
@ -74,9 +73,8 @@ def authenticated(func, *args, **kwargs):
|
||||
endpoints setup with this decorator require the user to be signed in
|
||||
"""
|
||||
request = args[1]
|
||||
if not request.keystone_user.get('authenticated', False):
|
||||
return Response({'errors': ["Credentials incorrect or none given."]},
|
||||
401)
|
||||
if not request.keystone_user.get("authenticated", False):
|
||||
return Response({"errors": ["Credentials incorrect or none given."]}, 401)
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
@ -87,7 +85,7 @@ def minimal_duration(func, min_time=1, *args, **kwargs):
|
||||
Make a function (or API call) take at least some time.
|
||||
"""
|
||||
# doesn't apply during tests
|
||||
if 'test' in sys.argv:
|
||||
if "test" in sys.argv:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
start = datetime.utcnow()
|
||||
|
@ -30,15 +30,15 @@ from adjutant.config import CONF
|
||||
|
||||
class UserList(tasks.InviteUser):
|
||||
|
||||
url = r'^openstack/users/?$'
|
||||
url = r"^openstack/users/?$"
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
'blacklisted_roles',
|
||||
"blacklisted_roles",
|
||||
help_text="Users with any of these roles will be hidden from the user list.",
|
||||
default=[],
|
||||
sample_default=['admin']
|
||||
sample_default=["admin"],
|
||||
),
|
||||
]
|
||||
)
|
||||
@ -51,11 +51,12 @@ class UserList(tasks.InviteUser):
|
||||
|
||||
user_list = []
|
||||
id_manager = user_store.IdentityManager()
|
||||
project_id = request.keystone_user['project_id']
|
||||
project_id = request.keystone_user["project_id"]
|
||||
project = id_manager.get_project(project_id)
|
||||
|
||||
can_manage_roles = id_manager.get_manageable_roles(
|
||||
request.keystone_user['roles'])
|
||||
request.keystone_user["roles"]
|
||||
)
|
||||
|
||||
active_emails = set()
|
||||
for user in id_manager.list_users(project):
|
||||
@ -77,20 +78,22 @@ class UserList(tasks.InviteUser):
|
||||
if skip:
|
||||
continue
|
||||
|
||||
email = getattr(user, 'email', '')
|
||||
email = getattr(user, "email", "")
|
||||
enabled = user.enabled
|
||||
user_status = 'Active' if enabled else 'Account Disabled'
|
||||
user_status = "Active" if enabled else "Account Disabled"
|
||||
active_emails.add(email)
|
||||
user_list.append({
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
'email': email,
|
||||
'roles': roles,
|
||||
'inherited_roles': inherited_roles,
|
||||
'cohort': 'Member',
|
||||
'status': user_status,
|
||||
'manageable': set(can_manage_roles).issuperset(roles),
|
||||
})
|
||||
user_list.append(
|
||||
{
|
||||
"id": user.id,
|
||||
"name": user.name,
|
||||
"email": email,
|
||||
"roles": roles,
|
||||
"inherited_roles": inherited_roles,
|
||||
"cohort": "Member",
|
||||
"status": user_status,
|
||||
"manageable": set(can_manage_roles).issuperset(roles),
|
||||
}
|
||||
)
|
||||
|
||||
for user in id_manager.list_inherited_users(project):
|
||||
skip = False
|
||||
@ -103,25 +106,29 @@ class UserList(tasks.InviteUser):
|
||||
if skip:
|
||||
continue
|
||||
|
||||
email = getattr(user, 'email', '')
|
||||
email = getattr(user, "email", "")
|
||||
enabled = user.enabled
|
||||
user_status = 'Active' if enabled else 'Account Disabled'
|
||||
user_list.append({'id': user.id,
|
||||
'name': user.name,
|
||||
'email': email,
|
||||
'roles': roles,
|
||||
'inherited_roles': [],
|
||||
'cohort': 'Inherited',
|
||||
'status': user_status,
|
||||
'manageable': False,
|
||||
})
|
||||
user_status = "Active" if enabled else "Account Disabled"
|
||||
user_list.append(
|
||||
{
|
||||
"id": user.id,
|
||||
"name": user.name,
|
||||
"email": email,
|
||||
"roles": roles,
|
||||
"inherited_roles": [],
|
||||
"cohort": "Inherited",
|
||||
"status": user_status,
|
||||
"manageable": False,
|
||||
}
|
||||
)
|
||||
|
||||
# Get my active tasks for this project:
|
||||
project_tasks = models.Task.objects.filter(
|
||||
project_id=project_id,
|
||||
task_type="invite_user_to_project",
|
||||
completed=0,
|
||||
cancelled=0)
|
||||
cancelled=0,
|
||||
)
|
||||
|
||||
registrations = []
|
||||
for task in project_tasks:
|
||||
@ -143,7 +150,8 @@ class UserList(tasks.InviteUser):
|
||||
task_data.update(action.action_data)
|
||||
|
||||
registrations.append(
|
||||
{'uuid': task.uuid, 'task_data': task_data, 'status': status})
|
||||
{"uuid": task.uuid, "task_data": task_data, "status": status}
|
||||
)
|
||||
|
||||
for task in registrations:
|
||||
# NOTE(adriant): commenting out for now as it causes more confusion
|
||||
@ -151,34 +159,33 @@ class UserList(tasks.InviteUser):
|
||||
# measures are in place.
|
||||
# if task['task_data']['email'] not in active_emails:
|
||||
user = {
|
||||
'id': task['uuid'],
|
||||
'name': task['task_data']['email'],
|
||||
'email': task['task_data']['email'],
|
||||
'roles': task['task_data']['roles'],
|
||||
'inherited_roles':
|
||||
task['task_data']['inherited_roles'],
|
||||
'cohort': 'Invited',
|
||||
'status': task['status']
|
||||
"id": task["uuid"],
|
||||
"name": task["task_data"]["email"],
|
||||
"email": task["task_data"]["email"],
|
||||
"roles": task["task_data"]["roles"],
|
||||
"inherited_roles": task["task_data"]["inherited_roles"],
|
||||
"cohort": "Invited",
|
||||
"status": task["status"],
|
||||
}
|
||||
if not CONF.identity.username_is_email:
|
||||
user['name'] = task['task_data']['username']
|
||||
user["name"] = task["task_data"]["username"]
|
||||
|
||||
user_list.append(user)
|
||||
|
||||
return Response({'users': user_list})
|
||||
return Response({"users": user_list})
|
||||
|
||||
|
||||
class UserDetail(BaseDelegateAPI):
|
||||
|
||||
url = r'^openstack/users/(?P<user_id>\w+)/?$'
|
||||
url = r"^openstack/users/(?P<user_id>\w+)/?$"
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
'blacklisted_roles',
|
||||
"blacklisted_roles",
|
||||
help_text="User with these roles will return not found.",
|
||||
default=[],
|
||||
sample_default=['admin']
|
||||
sample_default=["admin"],
|
||||
),
|
||||
]
|
||||
)
|
||||
@ -193,30 +200,34 @@ class UserDetail(BaseDelegateAPI):
|
||||
id_manager = user_store.IdentityManager()
|
||||
user = id_manager.get_user(user_id)
|
||||
|
||||
no_user = {'errors': ['No user with this id.']}
|
||||
no_user = {"errors": ["No user with this id."]}
|
||||
if not user:
|
||||
return Response(no_user, status=404)
|
||||
|
||||
class_conf = self.config
|
||||
blacklisted_roles = class_conf.blacklisted_roles
|
||||
|
||||
project_id = request.keystone_user['project_id']
|
||||
project_id = request.keystone_user["project_id"]
|
||||
project = id_manager.get_project(project_id)
|
||||
|
||||
roles = [role.name for role in id_manager.get_roles(user, project)]
|
||||
roles_blacklisted = set(blacklisted_roles) & set(roles)
|
||||
inherited_roles = [
|
||||
role.name for role in id_manager.get_roles(user, project, True)]
|
||||
inherited_roles_blacklisted = (
|
||||
set(blacklisted_roles) & set(inherited_roles))
|
||||
role.name for role in id_manager.get_roles(user, project, True)
|
||||
]
|
||||
inherited_roles_blacklisted = set(blacklisted_roles) & set(inherited_roles)
|
||||
|
||||
if not roles or roles_blacklisted or inherited_roles_blacklisted:
|
||||
return Response(no_user, status=404)
|
||||
return Response({'id': user.id,
|
||||
"username": user.name,
|
||||
"email": getattr(user, 'email', ''),
|
||||
'roles': roles,
|
||||
'inherited_roles': inherited_roles})
|
||||
return Response(
|
||||
{
|
||||
"id": user.id,
|
||||
"username": user.name,
|
||||
"email": getattr(user, "email", ""),
|
||||
"roles": roles,
|
||||
"inherited_roles": inherited_roles,
|
||||
}
|
||||
)
|
||||
|
||||
@utils.mod_or_admin
|
||||
def delete(self, request, user_id):
|
||||
@ -226,37 +237,42 @@ class UserDetail(BaseDelegateAPI):
|
||||
"""
|
||||
id_manager = user_store.IdentityManager()
|
||||
user = id_manager.get_user(user_id)
|
||||
project_id = request.keystone_user['project_id']
|
||||
project_id = request.keystone_user["project_id"]
|
||||
# NOTE(dale): For now, we only support cancelling pending invites.
|
||||
if user:
|
||||
return Response(
|
||||
{'errors': [
|
||||
'Revoking keystone users not implemented. '
|
||||
'Try removing all roles instead.']},
|
||||
status=501)
|
||||
{
|
||||
"errors": [
|
||||
"Revoking keystone users not implemented. "
|
||||
"Try removing all roles instead."
|
||||
]
|
||||
},
|
||||
status=501,
|
||||
)
|
||||
project_tasks = models.Task.objects.filter(
|
||||
project_id=project_id,
|
||||
task_type="invite_user_to_project",
|
||||
completed=0,
|
||||
cancelled=0)
|
||||
cancelled=0,
|
||||
)
|
||||
for task in project_tasks:
|
||||
if task.uuid == user_id:
|
||||
self.task_manager.cancel(task)
|
||||
return Response('Cancelled pending invite task!', status=200)
|
||||
return Response('Not found.', status=404)
|
||||
return Response("Cancelled pending invite task!", status=200)
|
||||
return Response("Not found.", status=404)
|
||||
|
||||
|
||||
class UserRoles(BaseDelegateAPI):
|
||||
|
||||
url = r'^openstack/users/(?P<user_id>\w+)/roles/?$'
|
||||
url = r"^openstack/users/(?P<user_id>\w+)/roles/?$"
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.ListConfig(
|
||||
'blacklisted_roles',
|
||||
"blacklisted_roles",
|
||||
help_text="User with these roles will return not found.",
|
||||
default=[],
|
||||
sample_default=['admin']
|
||||
sample_default=["admin"],
|
||||
),
|
||||
]
|
||||
)
|
||||
@ -269,11 +285,11 @@ class UserRoles(BaseDelegateAPI):
|
||||
id_manager = user_store.IdentityManager()
|
||||
user = id_manager.get_user(user_id)
|
||||
|
||||
no_user = {'errors': ['No user with this id.']}
|
||||
no_user = {"errors": ["No user with this id."]}
|
||||
if not user:
|
||||
return Response(no_user, status=404)
|
||||
|
||||
project_id = request.keystone_user['project_id']
|
||||
project_id = request.keystone_user["project_id"]
|
||||
project = id_manager.get_project(project_id)
|
||||
|
||||
class_conf = self.config
|
||||
@ -282,19 +298,18 @@ class UserRoles(BaseDelegateAPI):
|
||||
roles = [role.name for role in id_manager.get_roles(user, project)]
|
||||
roles_blacklisted = set(blacklisted_roles) & set(roles)
|
||||
inherited_roles = [
|
||||
role.name for role in id_manager.get_roles(user, project, True)]
|
||||
inherited_roles_blacklisted = (
|
||||
set(blacklisted_roles) & set(inherited_roles))
|
||||
role.name for role in id_manager.get_roles(user, project, True)
|
||||
]
|
||||
inherited_roles_blacklisted = set(blacklisted_roles) & set(inherited_roles)
|
||||
|
||||
if not roles or roles_blacklisted or inherited_roles_blacklisted:
|
||||
return Response(no_user, status=404)
|
||||
return Response({'roles': roles,
|
||||
'inherited_roles': inherited_roles})
|
||||
return Response({"roles": roles, "inherited_roles": inherited_roles})
|
||||
|
||||
@utils.mod_or_admin
|
||||
def put(self, args, **kwargs):
|
||||
""" Add user roles to the current project. """
|
||||
kwargs['remove_role'] = False
|
||||
kwargs["remove_role"] = False
|
||||
return self._edit_user(args, **kwargs)
|
||||
|
||||
@utils.mod_or_admin
|
||||
@ -303,34 +318,35 @@ class UserRoles(BaseDelegateAPI):
|
||||
|
||||
This only supports Active users
|
||||
"""
|
||||
kwargs['remove_role'] = True
|
||||
kwargs["remove_role"] = True
|
||||
return self._edit_user(args, **kwargs)
|
||||
|
||||
def _edit_user(self, request, user_id, remove_role=False, format=None):
|
||||
""" Helper function to add or remove roles from a user """
|
||||
request.data['remove'] = remove_role
|
||||
if 'project_id' not in request.data:
|
||||
request.data['project_id'] = request.keystone_user['project_id']
|
||||
request.data['user_id'] = user_id
|
||||
request.data["remove"] = remove_role
|
||||
if "project_id" not in request.data:
|
||||
request.data["project_id"] = request.keystone_user["project_id"]
|
||||
request.data["user_id"] = user_id
|
||||
|
||||
self.logger.info("(%s) - New EditUser %s request." % (
|
||||
timezone.now(), request.method))
|
||||
self.logger.info(
|
||||
"(%s) - New EditUser %s request." % (timezone.now(), request.method)
|
||||
)
|
||||
|
||||
self.task_manager.create_from_request(self.task_type, request)
|
||||
|
||||
return Response({'notes': ['task created']}, status=202)
|
||||
return Response({"notes": ["task created"]}, status=202)
|
||||
|
||||
|
||||
class RoleList(BaseDelegateAPI):
|
||||
|
||||
url = r'^openstack/roles/?$'
|
||||
url = r"^openstack/roles/?$"
|
||||
|
||||
@utils.mod_or_admin
|
||||
def get(self, request):
|
||||
"""Returns a list of roles that may be managed for this project"""
|
||||
|
||||
# get roles for this user on the project
|
||||
user_roles = request.keystone_user['roles']
|
||||
user_roles = request.keystone_user["roles"]
|
||||
|
||||
id_manager = user_store.IdentityManager()
|
||||
manageable_role_names = id_manager.get_manageable_roles(user_roles)
|
||||
@ -342,7 +358,7 @@ class RoleList(BaseDelegateAPI):
|
||||
if role:
|
||||
manageable_roles.append(role.to_dict())
|
||||
|
||||
return Response({'roles': manageable_roles})
|
||||
return Response({"roles": manageable_roles})
|
||||
|
||||
|
||||
class UserResetPassword(tasks.ResetPassword):
|
||||
@ -351,7 +367,7 @@ class UserResetPassword(tasks.ResetPassword):
|
||||
---
|
||||
"""
|
||||
|
||||
url = r'^openstack/users/password-reset/?$'
|
||||
url = r"^openstack/users/password-reset/?$"
|
||||
|
||||
pass
|
||||
|
||||
@ -362,7 +378,7 @@ class UserUpdateEmail(tasks.UpdateEmail):
|
||||
---
|
||||
"""
|
||||
|
||||
url = r'^openstack/users/email-update/?$'
|
||||
url = r"^openstack/users/email-update/?$"
|
||||
|
||||
pass
|
||||
|
||||
@ -372,7 +388,7 @@ class SignUp(tasks.CreateProjectAndUser):
|
||||
The openstack endpoint for signups.
|
||||
"""
|
||||
|
||||
url = r'^openstack/sign-up/?$'
|
||||
url = r"^openstack/sign-up/?$"
|
||||
|
||||
pass
|
||||
|
||||
@ -383,7 +399,7 @@ class UpdateProjectQuotas(BaseDelegateAPI):
|
||||
one or more regions
|
||||
"""
|
||||
|
||||
url = r'^openstack/quotas/?$'
|
||||
url = r"^openstack/quotas/?$"
|
||||
|
||||
task_type = "update_quota"
|
||||
|
||||
@ -395,7 +411,7 @@ class UpdateProjectQuotas(BaseDelegateAPI):
|
||||
task_type__exact=self.task_type,
|
||||
project_id__exact=self.project_id,
|
||||
cancelled=0,
|
||||
).order_by('-created_on')[:self._number_of_returned_tasks]
|
||||
).order_by("-created_on")[: self._number_of_returned_tasks]
|
||||
|
||||
response_tasks = []
|
||||
|
||||
@ -409,13 +425,12 @@ class UpdateProjectQuotas(BaseDelegateAPI):
|
||||
task_data.update(action.action_data)
|
||||
new_dict = {
|
||||
"id": task.uuid,
|
||||
"regions": task_data['regions'],
|
||||
"size": task_data['size'],
|
||||
"request_user":
|
||||
task.keystone_user['username'],
|
||||
"regions": task_data["regions"],
|
||||
"size": task_data["size"],
|
||||
"request_user": task.keystone_user["username"],
|
||||
"task_created": task.created_on,
|
||||
"valid": all([a.valid for a in task.actions]),
|
||||
"status": status
|
||||
"status": status,
|
||||
}
|
||||
response_tasks.append(new_dict)
|
||||
|
||||
@ -439,9 +454,9 @@ class UpdateProjectQuotas(BaseDelegateAPI):
|
||||
quota_sizes = CONF.quota.sizes
|
||||
size_order = CONF.quota.sizes_ascending
|
||||
|
||||
self.project_id = request.keystone_user['project_id']
|
||||
regions = request.query_params.get('regions', None)
|
||||
include_usage = request.query_params.get('include_usage', True)
|
||||
self.project_id = request.keystone_user["project_id"]
|
||||
regions = request.query_params.get("regions", None)
|
||||
include_usage = request.query_params.get("include_usage", True)
|
||||
|
||||
if regions:
|
||||
regions = regions.split(",")
|
||||
@ -456,35 +471,38 @@ class UpdateProjectQuotas(BaseDelegateAPI):
|
||||
quota_manager = QuotaManager(self.project_id)
|
||||
for region in regions:
|
||||
if self.check_region_exists(region):
|
||||
region_quotas.append(quota_manager.get_region_quota_data(
|
||||
region, include_usage))
|
||||
region_quotas.append(
|
||||
quota_manager.get_region_quota_data(region, include_usage)
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{"ERROR": ['Region: %s is not valid' % region]}, 400)
|
||||
return Response({"ERROR": ["Region: %s is not valid" % region]}, 400)
|
||||
|
||||
response_tasks = self.get_active_quota_tasks()
|
||||
|
||||
return Response({'regions': region_quotas,
|
||||
"quota_sizes": quota_sizes,
|
||||
"quota_size_order": size_order,
|
||||
"active_quota_tasks": response_tasks})
|
||||
return Response(
|
||||
{
|
||||
"regions": region_quotas,
|
||||
"quota_sizes": quota_sizes,
|
||||
"quota_size_order": size_order,
|
||||
"active_quota_tasks": response_tasks,
|
||||
}
|
||||
)
|
||||
|
||||
@utils.mod_or_admin
|
||||
def post(self, request):
|
||||
|
||||
request.data['project_id'] = request.keystone_user['project_id']
|
||||
self.project_id = request.keystone_user['project_id']
|
||||
request.data["project_id"] = request.keystone_user["project_id"]
|
||||
self.project_id = request.keystone_user["project_id"]
|
||||
|
||||
regions = request.data.get('regions', None)
|
||||
regions = request.data.get("regions", None)
|
||||
|
||||
if not regions:
|
||||
id_manager = user_store.IdentityManager()
|
||||
regions = [region.id for region in id_manager.list_regions()]
|
||||
request.data['regions'] = regions
|
||||
request.data["regions"] = regions
|
||||
|
||||
self.logger.info("(%s) - New UpdateProjectQuotas request."
|
||||
% timezone.now())
|
||||
self.logger.info("(%s) - New UpdateProjectQuotas request." % timezone.now())
|
||||
|
||||
self.task_manager.create_from_request(self.task_type, request)
|
||||
|
||||
return Response({'notes': ['task created']}, status=202)
|
||||
return Response({"notes": ["task created"]}, status=202)
|
||||
|
@ -27,14 +27,15 @@ from adjutant.api.v1.base import BaseDelegateAPI
|
||||
# NOTE(adriant): We should deprecate these Views properly and switch tests
|
||||
# to work against the openstack ones.
|
||||
|
||||
|
||||
class CreateProjectAndUser(BaseDelegateAPI):
|
||||
|
||||
url = r'^actions/CreateProjectAndUser/?$'
|
||||
url = r"^actions/CreateProjectAndUser/?$"
|
||||
|
||||
config_group = groups.DynamicNameConfigGroup(
|
||||
children=[
|
||||
fields.StrConfig(
|
||||
'default_region',
|
||||
"default_region",
|
||||
help_text="Default region in which any potential resources may be created.",
|
||||
required=True,
|
||||
default="RegionOne",
|
||||
@ -48,9 +49,9 @@ class CreateProjectAndUser(BaseDelegateAPI):
|
||||
fields.StrConfig(
|
||||
"default_parent_id",
|
||||
help_text="Parent id under which this project will be created. "
|
||||
"Default is None, and will create under default domain.",
|
||||
"Default is None, and will create under default domain.",
|
||||
default=None,
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@ -64,28 +65,27 @@ class CreateProjectAndUser(BaseDelegateAPI):
|
||||
incoming data and create a task to be approved
|
||||
later.
|
||||
"""
|
||||
self.logger.info(
|
||||
"(%s) - Starting new project task." % timezone.now())
|
||||
self.logger.info("(%s) - Starting new project task." % timezone.now())
|
||||
|
||||
class_conf = self.config
|
||||
|
||||
# we need to set the region the resources will be created in:
|
||||
request.data['region'] = class_conf.default_region
|
||||
request.data["region"] = class_conf.default_region
|
||||
|
||||
# domain
|
||||
request.data['domain_id'] = class_conf.default_domain_id
|
||||
request.data["domain_id"] = class_conf.default_domain_id
|
||||
|
||||
# parent_id for new project, if null defaults to domain:
|
||||
request.data['parent_id'] = class_conf.default_parent_id
|
||||
request.data["parent_id"] = class_conf.default_parent_id
|
||||
|
||||
self.task_manager.create_from_request(self.task_type, request)
|
||||
|
||||
return Response({'notes': ['task created']}, status=202)
|
||||
return Response({"notes": ["task created"]}, status=202)
|
||||
|
||||
|
||||
class InviteUser(BaseDelegateAPI):
|
||||
|
||||
url = r'^actions/InviteUser/?$'
|
||||
url = r"^actions/InviteUser/?$"
|
||||
|
||||
task_type = "invite_user_to_project"
|
||||
|
||||
@ -105,24 +105,21 @@ class InviteUser(BaseDelegateAPI):
|
||||
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):
|
||||
request.data['project_id'] = request.keystone_user['project_id']
|
||||
if "project_id" not in request.data or request.data["project_id"] is None:
|
||||
request.data["project_id"] = request.keystone_user["project_id"]
|
||||
|
||||
# Default domain_id to the keystone user's project
|
||||
if ('domain_id' not in request.data
|
||||
or request.data['domain_id'] is None):
|
||||
request.data['domain_id'] = \
|
||||
request.keystone_user['project_domain_id']
|
||||
if "domain_id" not in request.data or request.data["domain_id"] is None:
|
||||
request.data["domain_id"] = request.keystone_user["project_domain_id"]
|
||||
|
||||
self.task_manager.create_from_request(self.task_type, request)
|
||||
|
||||
return Response({'notes': ['task created']}, status=202)
|
||||
return Response({"notes": ["task created"]}, status=202)
|
||||
|
||||
|
||||
class ResetPassword(BaseDelegateAPI):
|
||||
|
||||
url = r'^actions/ResetPassword/?$'
|
||||
url = r"^actions/ResetPassword/?$"
|
||||
|
||||
task_type = "reset_user_password"
|
||||
|
||||
@ -156,17 +153,19 @@ class ResetPassword(BaseDelegateAPI):
|
||||
self.task_manager.create_from_request(self.task_type, request)
|
||||
except exceptions.BaseTaskException as e:
|
||||
self.logger.info(
|
||||
"(%s) - ResetPassword raised error: %s" % (timezone.now(), e))
|
||||
"(%s) - ResetPassword raised error: %s" % (timezone.now(), e)
|
||||
)
|
||||
|
||||
response_dict = {'notes': [
|
||||
"If user with email exists, reset token will be issued."]}
|
||||
response_dict = {
|
||||
"notes": ["If user with email exists, reset token will be issued."]
|
||||
}
|
||||
|
||||
return Response(response_dict, status=202)
|
||||
|
||||
|
||||
class EditUser(BaseDelegateAPI):
|
||||
|
||||
url = r'^actions/EditUser/?$'
|
||||
url = r"^actions/EditUser/?$"
|
||||
|
||||
task_type = "edit_user_roles"
|
||||
|
||||
@ -183,12 +182,12 @@ class EditUser(BaseDelegateAPI):
|
||||
|
||||
self.task_manager.create_from_request(self.task_type, request)
|
||||
|
||||
return Response({'notes': ['task created']}, status=202)
|
||||
return Response({"notes": ["task created"]}, status=202)
|
||||
|
||||
|
||||
class UpdateEmail(BaseDelegateAPI):
|
||||
|
||||
url = r'^actions/UpdateEmail/?$'
|
||||
url = r"^actions/UpdateEmail/?$"
|
||||
|
||||
task_type = "update_user_email"
|
||||
|
||||
@ -199,8 +198,8 @@ class UpdateEmail(BaseDelegateAPI):
|
||||
This will submit and approve an update email action.
|
||||
"""
|
||||
|
||||
request.data['user_id'] = request.keystone_user['user_id']
|
||||
request.data["user_id"] = request.keystone_user["user_id"]
|
||||
|
||||
self.task_manager.create_from_request(self.task_type, request)
|
||||
|
||||
return Response({'notes': ['task created']}, status=202)
|
||||
return Response({"notes": ["task created"]}, status=202)
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -19,19 +19,16 @@ from adjutant import api
|
||||
from adjutant.config import CONF
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^status/?$', views.StatusView.as_view()),
|
||||
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()),
|
||||
url(r'^tokens/?$', views.TokenList.as_view()),
|
||||
url(r'^notifications/(?P<uuid>\w+)/?$',
|
||||
views.NotificationDetail.as_view()),
|
||||
url(r'^notifications/?$', views.NotificationList.as_view()),
|
||||
url(r"^status/?$", views.StatusView.as_view()),
|
||||
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()),
|
||||
url(r"^tokens/?$", views.TokenList.as_view()),
|
||||
url(r"^notifications/(?P<uuid>\w+)/?$", views.NotificationDetail.as_view()),
|
||||
url(r"^notifications/?$", views.NotificationList.as_view()),
|
||||
]
|
||||
|
||||
for active_view in CONF.api.active_delegate_apis:
|
||||
delegate_api = api.DELEGATE_API_CLASSES[active_view]
|
||||
|
||||
urlpatterns.append(
|
||||
url(delegate_api.url, delegate_api.as_view())
|
||||
)
|
||||
urlpatterns.append(url(delegate_api.url, delegate_api.as_view()))
|
||||
|
@ -31,7 +31,7 @@ def parse_filters(func, *args, **kwargs):
|
||||
BE AWARE! WILL NOT WORK UNLESS POSITIONAL ARGUMENT 3 IS FILTERS!
|
||||
"""
|
||||
request = args[1]
|
||||
filters = request.query_params.get('filters', None)
|
||||
filters = request.query_params.get("filters", None)
|
||||
|
||||
if not filters:
|
||||
return func(*args, **kwargs)
|
||||
@ -40,14 +40,16 @@ def parse_filters(func, *args, **kwargs):
|
||||
filters = json.loads(filters)
|
||||
for field, operations in filters.items():
|
||||
for operation, value in operations.items():
|
||||
cleaned_filters['%s__%s' % (field, operation)] = value
|
||||
cleaned_filters["%s__%s" % (field, operation)] = value
|
||||
except (ValueError, AttributeError):
|
||||
return Response(
|
||||
{'errors': [
|
||||
"Filters incorrectly formatted. Required format: "
|
||||
"{'filters': {'fieldname': { 'operation': 'value'}}"
|
||||
]},
|
||||
status=400
|
||||
{
|
||||
"errors": [
|
||||
"Filters incorrectly formatted. Required format: "
|
||||
"{'filters': {'fieldname': { 'operation': 'value'}}"
|
||||
]
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
@ -57,4 +59,4 @@ def parse_filters(func, *args, **kwargs):
|
||||
args[2] = cleaned_filters
|
||||
return func(*args, **kwargs)
|
||||
except FieldError as e:
|
||||
return Response({'errors': [str(e)]}, status=400)
|
||||
return Response({"errors": [str(e)]}, status=400)
|
||||
|
@ -31,21 +31,21 @@ from adjutant.tasks.models import Task
|
||||
|
||||
|
||||
class V1VersionEndpoint(SingleVersionView):
|
||||
version = '1.0'
|
||||
version = "1.0"
|
||||
|
||||
|
||||
class APIViewWithLogger(APIView):
|
||||
"""
|
||||
APIView with a logger.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(APIViewWithLogger, self).__init__(*args, **kwargs)
|
||||
self.logger = getLogger('adjutant')
|
||||
self.logger = getLogger("adjutant")
|
||||
self.task_manager = TaskManager()
|
||||
|
||||
|
||||
class StatusView(APIViewWithLogger):
|
||||
|
||||
@utils.admin
|
||||
def get(self, request, filters=None, format=None):
|
||||
"""
|
||||
@ -56,33 +56,31 @@ class StatusView(APIViewWithLogger):
|
||||
|
||||
Can returns None, if there are no tasks.
|
||||
"""
|
||||
notifications = Notification.objects.filter(
|
||||
error=1,
|
||||
acknowledged=0
|
||||
)
|
||||
notifications = Notification.objects.filter(error=1, acknowledged=0)
|
||||
|
||||
try:
|
||||
last_created_task = Task.objects.filter(
|
||||
completed=0).order_by("-created_on")[0].to_dict()
|
||||
last_created_task = (
|
||||
Task.objects.filter(completed=0).order_by("-created_on")[0].to_dict()
|
||||
)
|
||||
except IndexError:
|
||||
last_created_task = None
|
||||
try:
|
||||
last_completed_task = Task.objects.filter(
|
||||
completed=1).order_by("-completed_on")[0].to_dict()
|
||||
last_completed_task = (
|
||||
Task.objects.filter(completed=1).order_by("-completed_on")[0].to_dict()
|
||||
)
|
||||
except IndexError:
|
||||
last_completed_task = None
|
||||
|
||||
status = {
|
||||
"error_notifications": [note.to_dict() for note in notifications],
|
||||
"last_created_task": last_created_task,
|
||||
"last_completed_task": last_completed_task
|
||||
"last_completed_task": last_completed_task,
|
||||
}
|
||||
|
||||
return Response(status, status=200)
|
||||
|
||||
|
||||
class NotificationList(APIViewWithLogger):
|
||||
|
||||
@utils.admin
|
||||
@parse_filters
|
||||
def get(self, request, filters=None, format=None):
|
||||
@ -90,33 +88,37 @@ class NotificationList(APIViewWithLogger):
|
||||
A list of Notification objects as dicts.
|
||||
"""
|
||||
if filters:
|
||||
notifications = Notification.objects.filter(
|
||||
**filters).order_by("-created_on")
|
||||
notifications = Notification.objects.filter(**filters).order_by(
|
||||
"-created_on"
|
||||
)
|
||||
else:
|
||||
notifications = Notification.objects.all().order_by("-created_on")
|
||||
|
||||
page = request.GET.get('page', 1)
|
||||
notifs_per_page = request.GET.get('notifications_per_page', None)
|
||||
page = request.GET.get("page", 1)
|
||||
notifs_per_page = request.GET.get("notifications_per_page", None)
|
||||
|
||||
if notifs_per_page:
|
||||
paginator = Paginator(notifications, notifs_per_page)
|
||||
try:
|
||||
notifications = paginator.page(page)
|
||||
except EmptyPage:
|
||||
return Response({'errors': ['Empty page']}, status=400)
|
||||
return Response({"errors": ["Empty page"]}, status=400)
|
||||
except PageNotAnInteger:
|
||||
return Response({'errors': ['Page not an integer']},
|
||||
status=400)
|
||||
return Response({"errors": ["Page not an integer"]}, status=400)
|
||||
|
||||
note_list = []
|
||||
for notification in notifications:
|
||||
note_list.append(notification.to_dict())
|
||||
if notifs_per_page:
|
||||
return Response({'notifications': note_list,
|
||||
'pages': paginator.num_pages,
|
||||
'has_more': notifications.has_next(),
|
||||
'has_prev': notifications.has_previous()},
|
||||
status=200)
|
||||
return Response(
|
||||
{
|
||||
"notifications": note_list,
|
||||
"pages": paginator.num_pages,
|
||||
"has_more": notifications.has_next(),
|
||||
"has_prev": notifications.has_previous(),
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
return Response({"notifications": note_list}, status=200)
|
||||
|
||||
@ -125,24 +127,21 @@ class NotificationList(APIViewWithLogger):
|
||||
"""
|
||||
Acknowledge notifications.
|
||||
"""
|
||||
note_list = request.data.get('notifications', None)
|
||||
note_list = request.data.get("notifications", None)
|
||||
if note_list and isinstance(note_list, list):
|
||||
notifications = Notification.objects.filter(uuid__in=note_list)
|
||||
for notification in notifications:
|
||||
notification.acknowledged = True
|
||||
notification.save()
|
||||
return Response({'notes': ['Notifications acknowledged.']},
|
||||
status=200)
|
||||
return Response({"notes": ["Notifications acknowledged."]}, status=200)
|
||||
else:
|
||||
return Response(
|
||||
{'notifications': [
|
||||
"this field is required and needs to be a list."
|
||||
]},
|
||||
status=400)
|
||||
{"notifications": ["this field is required and needs to be a list."]},
|
||||
status=400,
|
||||
)
|
||||
|
||||
|
||||
class NotificationDetail(APIViewWithLogger):
|
||||
|
||||
@utils.admin
|
||||
def get(self, request, uuid, format=None):
|
||||
"""
|
||||
@ -152,9 +151,7 @@ class NotificationDetail(APIViewWithLogger):
|
||||
try:
|
||||
notification = Notification.objects.get(uuid=uuid)
|
||||
except Notification.DoesNotExist:
|
||||
return Response(
|
||||
{'errors': ['No notification with this id.']},
|
||||
status=404)
|
||||
return Response({"errors": ["No notification with this id."]}, status=404)
|
||||
return Response(notification.to_dict())
|
||||
|
||||
@utils.admin
|
||||
@ -165,25 +162,21 @@ class NotificationDetail(APIViewWithLogger):
|
||||
try:
|
||||
notification = Notification.objects.get(uuid=uuid)
|
||||
except Notification.DoesNotExist:
|
||||
return Response(
|
||||
{'errors': ['No notification with this id.']},
|
||||
status=404)
|
||||
return Response({"errors": ["No notification with this id."]}, status=404)
|
||||
|
||||
if notification.acknowledged:
|
||||
return Response({'notes': ['Notification already acknowledged.']},
|
||||
status=200)
|
||||
if request.data.get('acknowledged', False) is True:
|
||||
return Response(
|
||||
{"notes": ["Notification already acknowledged."]}, status=200
|
||||
)
|
||||
if request.data.get("acknowledged", False) is True:
|
||||
notification.acknowledged = True
|
||||
notification.save()
|
||||
return Response({'notes': ['Notification acknowledged.']},
|
||||
status=200)
|
||||
return Response({"notes": ["Notification acknowledged."]}, status=200)
|
||||
else:
|
||||
return Response({'acknowledged': ["this field is required."]},
|
||||
status=400)
|
||||
return Response({"acknowledged": ["this field is required."]}, status=400)
|
||||
|
||||
|
||||
class TaskList(APIViewWithLogger):
|
||||
|
||||
@utils.admin
|
||||
@parse_filters
|
||||
def get(self, request, filters=None, format=None):
|
||||
@ -192,20 +185,20 @@ class TaskList(APIViewWithLogger):
|
||||
and their related actions.
|
||||
"""
|
||||
|
||||
page = request.GET.get('page', 1)
|
||||
tasks_per_page = request.GET.get('tasks_per_page', None)
|
||||
page = request.GET.get("page", 1)
|
||||
tasks_per_page = request.GET.get("tasks_per_page", None)
|
||||
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
# TODO(adriant): better handle this bit of incode policy
|
||||
if 'admin' not in request.keystone_user['roles']:
|
||||
if "admin" not in request.keystone_user["roles"]:
|
||||
# Ignore any filters with project_id in them
|
||||
for field_filter in filters.keys():
|
||||
if "project_id" in field_filter:
|
||||
filters.pop(field_filter)
|
||||
|
||||
filters['project_id__exact'] = request.keystone_user['project_id']
|
||||
filters["project_id__exact"] = request.keystone_user["project_id"]
|
||||
|
||||
tasks = Task.objects.filter(**filters).order_by("-created_on")
|
||||
|
||||
@ -214,27 +207,30 @@ class TaskList(APIViewWithLogger):
|
||||
try:
|
||||
tasks = paginator.page(page)
|
||||
except EmptyPage:
|
||||
return Response({'errors': ['Empty page']}, status=400)
|
||||
return Response({"errors": ["Empty page"]}, status=400)
|
||||
except PageNotAnInteger:
|
||||
return Response({'errors': ['Page not an integer']},
|
||||
status=400)
|
||||
return Response({"errors": ["Page not an integer"]}, status=400)
|
||||
task_list = []
|
||||
for task in tasks:
|
||||
task_list.append(task.to_dict())
|
||||
|
||||
if tasks_per_page:
|
||||
return Response({'tasks': task_list,
|
||||
'pages': paginator.num_pages,
|
||||
'has_more': tasks.has_next(),
|
||||
'has_prev': tasks.has_previous()}, status=200)
|
||||
return Response(
|
||||
{
|
||||
"tasks": task_list,
|
||||
"pages": paginator.num_pages,
|
||||
"has_more": tasks.has_next(),
|
||||
"has_prev": tasks.has_previous(),
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
# NOTE(amelia): 'has_more'and 'has_prev' names are
|
||||
# based on the horizon pagination table pagination names
|
||||
else:
|
||||
return Response({'tasks': task_list})
|
||||
return Response({"tasks": task_list})
|
||||
|
||||
|
||||
class TaskDetail(APIViewWithLogger):
|
||||
|
||||
@utils.mod_or_admin
|
||||
def get(self, request, uuid, format=None):
|
||||
"""
|
||||
@ -243,16 +239,15 @@ class TaskDetail(APIViewWithLogger):
|
||||
"""
|
||||
try:
|
||||
# TODO(adriant): better handle this bit of incode policy
|
||||
if 'admin' in request.keystone_user['roles']:
|
||||
if "admin" in request.keystone_user["roles"]:
|
||||
task = Task.objects.get(uuid=uuid)
|
||||
else:
|
||||
task = Task.objects.get(
|
||||
uuid=uuid, project_id=request.keystone_user['project_id'])
|
||||
uuid=uuid, project_id=request.keystone_user["project_id"]
|
||||
)
|
||||
return Response(task.to_dict())
|
||||
except Task.DoesNotExist:
|
||||
return Response(
|
||||
{'errors': ['No task with this id.']},
|
||||
status=404)
|
||||
return Response({"errors": ["No task with this id."]}, status=404)
|
||||
|
||||
@utils.admin
|
||||
def put(self, request, uuid, format=None):
|
||||
@ -262,9 +257,7 @@ class TaskDetail(APIViewWithLogger):
|
||||
"""
|
||||
self.task_manager.update(uuid, request.data)
|
||||
|
||||
return Response(
|
||||
{'notes': ["Task successfully updated."]},
|
||||
status=200)
|
||||
return Response({"notes": ["Task successfully updated."]}, status=200)
|
||||
|
||||
@utils.admin
|
||||
def post(self, request, uuid, format=None):
|
||||
@ -274,21 +267,21 @@ class TaskDetail(APIViewWithLogger):
|
||||
and if valid will setup and create a related token.
|
||||
"""
|
||||
try:
|
||||
if request.data.get('approved') is not True:
|
||||
if request.data.get("approved") is not True:
|
||||
raise exceptions.TaskSerializersInvalid(
|
||||
{'approved': ["this is a required boolean field."]})
|
||||
{"approved": ["this is a required boolean field."]}
|
||||
)
|
||||
except ParseError:
|
||||
raise exceptions.TaskSerializersInvalid(
|
||||
{'approved': ["this is a required boolean field."]})
|
||||
{"approved": ["this is a required boolean field."]}
|
||||
)
|
||||
|
||||
task = self.task_manager.approve(uuid, request.keystone_user)
|
||||
|
||||
if task.completed:
|
||||
return Response(
|
||||
{'notes': ["Task completed successfully."]}, status=200)
|
||||
return Response({"notes": ["Task completed successfully."]}, status=200)
|
||||
else:
|
||||
return Response(
|
||||
{'notes': ['created token']}, status=202)
|
||||
return Response({"notes": ["created token"]}, status=202)
|
||||
|
||||
@utils.mod_or_admin
|
||||
def delete(self, request, uuid, format=None):
|
||||
@ -300,21 +293,18 @@ class TaskDetail(APIViewWithLogger):
|
||||
"""
|
||||
try:
|
||||
# TODO(adriant): better handle this bit of incode policy
|
||||
if 'admin' in request.keystone_user['roles']:
|
||||
if "admin" in request.keystone_user["roles"]:
|
||||
task = Task.objects.get(uuid=uuid)
|
||||
else:
|
||||
task = Task.objects.get(
|
||||
uuid=uuid, project_id=request.keystone_user['project_id'])
|
||||
uuid=uuid, project_id=request.keystone_user["project_id"]
|
||||
)
|
||||
except Task.DoesNotExist:
|
||||
return Response(
|
||||
{'errors': ['No task with this id.']},
|
||||
status=404)
|
||||
return Response({"errors": ["No task with this id."]}, status=404)
|
||||
|
||||
self.task_manager.cancel(task)
|
||||
|
||||
return Response(
|
||||
{'notes': ["Task cancelled successfully."]},
|
||||
status=200)
|
||||
return Response({"notes": ["Task cancelled successfully."]}, status=200)
|
||||
|
||||
|
||||
class TokenList(APIViewWithLogger):
|
||||
@ -344,26 +334,24 @@ class TokenList(APIViewWithLogger):
|
||||
|
||||
Clears other tokens for it.
|
||||
"""
|
||||
uuid = request.data.get('task', None)
|
||||
uuid = request.data.get("task", None)
|
||||
if uuid is None:
|
||||
return Response(
|
||||
{'errors': {'task': ["This field is required.", ]}},
|
||||
status=400)
|
||||
{"errors": {"task": ["This field is required.",]}}, status=400
|
||||
)
|
||||
try:
|
||||
# TODO(adriant): better handle this bit of incode policy
|
||||
if 'admin' in request.keystone_user['roles']:
|
||||
if "admin" in request.keystone_user["roles"]:
|
||||
task = Task.objects.get(uuid=uuid)
|
||||
else:
|
||||
task = Task.objects.get(
|
||||
uuid=uuid, project_id=request.keystone_user['project_id'])
|
||||
uuid=uuid, project_id=request.keystone_user["project_id"]
|
||||
)
|
||||
except Task.DoesNotExist:
|
||||
return Response(
|
||||
{'errors': ['No task with this id.']},
|
||||
status=404)
|
||||
return Response({"errors": ["No task with this id."]}, status=404)
|
||||
|
||||
self.task_manager.reissue_token(task)
|
||||
return Response(
|
||||
{'notes': ['Token reissued.']}, status=200)
|
||||
return Response({"notes": ["Token reissued."]}, status=200)
|
||||
|
||||
@utils.admin
|
||||
def delete(self, request, format=None):
|
||||
@ -372,12 +360,10 @@ class TokenList(APIViewWithLogger):
|
||||
"""
|
||||
now = timezone.now()
|
||||
Token.objects.filter(expires__lt=now).delete()
|
||||
return Response(
|
||||
{'notes': ['Deleted all expired tokens.']}, status=200)
|
||||
return Response({"notes": ["Deleted all expired tokens."]}, status=200)
|
||||
|
||||
|
||||
class TokenDetail(APIViewWithLogger):
|
||||
|
||||
def get(self, request, id, format=None):
|
||||
"""
|
||||
Returns a response with the list of required fields
|
||||
@ -390,20 +376,16 @@ class TokenDetail(APIViewWithLogger):
|
||||
token = Token.objects.get(token=id)
|
||||
except Token.DoesNotExist:
|
||||
return Response(
|
||||
{'errors': ['This token does not exist or has expired.']},
|
||||
status=404)
|
||||
{"errors": ["This token does not exist or has expired."]}, status=404
|
||||
)
|
||||
|
||||
if token.task.completed:
|
||||
return Response(
|
||||
{'errors':
|
||||
['This task has already been completed.']},
|
||||
status=400)
|
||||
{"errors": ["This task has already been completed."]}, status=400
|
||||
)
|
||||
|
||||
if token.task.cancelled:
|
||||
return Response(
|
||||
{'errors':
|
||||
['This task has been cancelled.']},
|
||||
status=400)
|
||||
return Response({"errors": ["This task has been cancelled."]}, status=400)
|
||||
|
||||
required_fields = []
|
||||
actions = []
|
||||
@ -415,9 +397,13 @@ class TokenDetail(APIViewWithLogger):
|
||||
if field not in required_fields:
|
||||
required_fields.append(field)
|
||||
|
||||
return Response({'actions': [str(act) for act in actions],
|
||||
'required_fields': required_fields,
|
||||
'task_type': token.task.task_type})
|
||||
return Response(
|
||||
{
|
||||
"actions": [str(act) for act in actions],
|
||||
"required_fields": required_fields,
|
||||
"task_type": token.task.task_type,
|
||||
}
|
||||
)
|
||||
|
||||
def post(self, request, id, format=None):
|
||||
"""
|
||||
@ -432,11 +418,9 @@ class TokenDetail(APIViewWithLogger):
|
||||
token = Token.objects.get(token=id)
|
||||
except Token.DoesNotExist:
|
||||
return Response(
|
||||
{'errors': ['This token does not exist or has expired.']},
|
||||
status=404)
|
||||
{"errors": ["This token does not exist or has expired."]}, status=404
|
||||
)
|
||||
|
||||
self.task_manager.submit(token.task, request.data)
|
||||
|
||||
return Response(
|
||||
{'notes': ["Token submitted successfully."]},
|
||||
status=200)
|
||||
return Response({"notes": ["Token submitted successfully."]}, status=200)
|
||||
|
@ -14,38 +14,31 @@ def build_version_details(id, status, links=None, relative_endpoint=None):
|
||||
relative_endpoint = "v%s/" % int_id
|
||||
mime_type = "application/vnd.openstack.adjutant-v%s+json" % int_id
|
||||
version_details = {
|
||||
'status': status,
|
||||
'id': id,
|
||||
'media-types': [
|
||||
{
|
||||
'base': 'application/json',
|
||||
'type': mime_type
|
||||
}
|
||||
],
|
||||
'links': []
|
||||
"status": status,
|
||||
"id": id,
|
||||
"media-types": [{"base": "application/json", "type": mime_type}],
|
||||
"links": [],
|
||||
}
|
||||
|
||||
if links:
|
||||
version_details['links'] = links
|
||||
version_details["links"] = links
|
||||
|
||||
version_details['relative_endpoint'] = relative_endpoint
|
||||
version_details["relative_endpoint"] = relative_endpoint
|
||||
_VERSIONS[id] = version_details
|
||||
return version_details
|
||||
|
||||
|
||||
class VersionView(APIView):
|
||||
|
||||
def get(self, request):
|
||||
versions = []
|
||||
for version in _VERSIONS.values():
|
||||
version = version.copy()
|
||||
rel_endpoint = version.pop('relative_endpoint')
|
||||
rel_endpoint = version.pop("relative_endpoint")
|
||||
url = request.build_absolute_uri() + rel_endpoint
|
||||
version['links'] = version['links'] + [{'href': url,
|
||||
'rel': 'self'}]
|
||||
version["links"] = version["links"] + [{"href": url, "rel": "self"}]
|
||||
versions.append(version)
|
||||
|
||||
return Response({'versions': versions}, status=200)
|
||||
return Response({"versions": versions}, status=200)
|
||||
|
||||
|
||||
class SingleVersionView(APIView):
|
||||
@ -58,11 +51,11 @@ class SingleVersionView(APIView):
|
||||
|
||||
version = _VERSIONS.get(self.version, {}).copy()
|
||||
if not version:
|
||||
return Response({'error': 'Not Found'}, status=404)
|
||||
return Response({"error": "Not Found"}, status=404)
|
||||
|
||||
version.pop('relative_endpoint')
|
||||
version.pop("relative_endpoint")
|
||||
|
||||
version['links'] = version['links'] + [
|
||||
{'href': request.build_absolute_uri(),
|
||||
'rel': 'self'}]
|
||||
return Response({'version': version}, status=200)
|
||||
version["links"] = version["links"] + [
|
||||
{"href": request.build_absolute_uri(), "rel": "self"}
|
||||
]
|
||||
return Response({"version": version}, status=200)
|
||||
|
@ -10,8 +10,8 @@ from adjutant import config
|
||||
def make_yaml_lines(val, depth, comment=False):
|
||||
new_lines = []
|
||||
line_prefix = " " * (depth + 1)
|
||||
for line in yaml.dump(val).split('\n'):
|
||||
if line == '':
|
||||
for line in yaml.dump(val).split("\n"):
|
||||
if line == "":
|
||||
continue
|
||||
if comment:
|
||||
new_lines.append(line_prefix + "# %s" % line)
|
||||
@ -28,7 +28,7 @@ def make_field_lines(field, depth):
|
||||
field_help_text = "# %s" % field.help_text
|
||||
field_lines.append(line_prefix + field_help_text)
|
||||
|
||||
default = ''
|
||||
default = ""
|
||||
if field.default is not None:
|
||||
default = field.default
|
||||
|
||||
@ -48,7 +48,7 @@ def make_field_lines(field, depth):
|
||||
else:
|
||||
field_lines.append(line_prefix + "# %s:" % field.name)
|
||||
else:
|
||||
if default == '':
|
||||
if default == "":
|
||||
field_lines.append(line_prefix + "# %s: <your_value>" % field.name)
|
||||
else:
|
||||
default_str = " " + str(default)
|
||||
@ -70,20 +70,20 @@ def make_group_lines(group, depth=0):
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = ''
|
||||
help = ""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--output-file', default="adjutant.yaml")
|
||||
parser.add_argument("--output-file", default="adjutant.yaml")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
print("Generating example file to: '%s'" % options['output_file'])
|
||||
print("Generating example file to: '%s'" % options["output_file"])
|
||||
|
||||
base_lines = []
|
||||
for group in config._root_config:
|
||||
base_lines += make_group_lines(group)
|
||||
base_lines.append("")
|
||||
|
||||
with open(options['output_file'], "w") as f:
|
||||
with open(options["output_file"], "w") as f:
|
||||
for line in base_lines:
|
||||
f.write(line)
|
||||
f.write("\n")
|
||||
|
@ -56,38 +56,25 @@ def get_auth_session():
|
||||
|
||||
|
||||
def get_keystoneclient(version=DEFAULT_IDENTITY_VERSION):
|
||||
return ks_client.Client(
|
||||
version,
|
||||
session=get_auth_session())
|
||||
return ks_client.Client(version, session=get_auth_session())
|
||||
|
||||
|
||||
def get_neutronclient(region):
|
||||
# always returns neutron client v2
|
||||
return neutronclient.Client(
|
||||
session=get_auth_session(),
|
||||
region_name=region)
|
||||
return neutronclient.Client(session=get_auth_session(), region_name=region)
|
||||
|
||||
|
||||
def get_novaclient(region, version=DEFAULT_COMPUTE_VERSION):
|
||||
return novaclient.Client(
|
||||
version,
|
||||
session=get_auth_session(),
|
||||
region_name=region)
|
||||
return novaclient.Client(version, session=get_auth_session(), region_name=region)
|
||||
|
||||
|
||||
def get_cinderclient(region, version=DEFAULT_VOLUME_VERSION):
|
||||
return cinderclient.Client(
|
||||
version,
|
||||
session=get_auth_session(),
|
||||
region_name=region)
|
||||
return cinderclient.Client(version, session=get_auth_session(), region_name=region)
|
||||
|
||||
|
||||
def get_octaviaclient(region):
|
||||
ks = get_keystoneclient()
|
||||
|
||||
service = ks.services.list(name='octavia')[0]
|
||||
endpoint = ks.endpoints.list(service=service,
|
||||
region=region, interface='public')[0]
|
||||
return octavia.OctaviaAPI(
|
||||
session=get_auth_session(),
|
||||
endpoint=endpoint.url)
|
||||
service = ks.services.list(name="octavia")[0]
|
||||
endpoint = ks.endpoints.list(service=service, region=region, interface="public")[0]
|
||||
return octavia.OctaviaAPI(session=get_auth_session(), endpoint=endpoint.url)
|
||||
|
@ -22,7 +22,7 @@ class QuotaManager(object):
|
||||
across all services.
|
||||
"""
|
||||
|
||||
default_size_diff_threshold = .2
|
||||
default_size_diff_threshold = 0.2
|
||||
|
||||
class ServiceQuotaHelper(object):
|
||||
def set_quota(self, values):
|
||||
@ -30,8 +30,7 @@ class QuotaManager(object):
|
||||
|
||||
class ServiceQuotaCinderHelper(ServiceQuotaHelper):
|
||||
def __init__(self, region_name, project_id):
|
||||
self.client = openstack_clients.get_cinderclient(
|
||||
region=region_name)
|
||||
self.client = openstack_clients.get_cinderclient(region=region_name)
|
||||
self.project_id = project_id
|
||||
|
||||
def get_quota(self):
|
||||
@ -39,39 +38,40 @@ class QuotaManager(object):
|
||||
|
||||
def get_usage(self):
|
||||
volumes = self.client.volumes.list(
|
||||
search_opts={'all_tenants': 1, 'project_id': self.project_id})
|
||||
search_opts={"all_tenants": 1, "project_id": self.project_id}
|
||||
)
|
||||
snapshots = self.client.volume_snapshots.list(
|
||||
search_opts={'all_tenants': 1, 'project_id': self.project_id})
|
||||
search_opts={"all_tenants": 1, "project_id": self.project_id}
|
||||
)
|
||||
|
||||
# gigabytesUsed should be a total of volumes and snapshots
|
||||
gigabytes = sum([getattr(volume, 'size', 0) for volume
|
||||
in volumes])
|
||||
gigabytes += sum([getattr(snap, 'size', 0) for snap
|
||||
in snapshots])
|
||||
gigabytes = sum([getattr(volume, "size", 0) for volume in volumes])
|
||||
gigabytes += sum([getattr(snap, "size", 0) for snap in snapshots])
|
||||
|
||||
return {'gigabytes': gigabytes,
|
||||
'volumes': len(volumes),
|
||||
'snapshots': len(snapshots)
|
||||
}
|
||||
return {
|
||||
"gigabytes": gigabytes,
|
||||
"volumes": len(volumes),
|
||||
"snapshots": len(snapshots),
|
||||
}
|
||||
|
||||
class ServiceQuotaNovaHelper(ServiceQuotaHelper):
|
||||
def __init__(self, region_name, project_id):
|
||||
self.client = openstack_clients.get_novaclient(
|
||||
region=region_name)
|
||||
self.client = openstack_clients.get_novaclient(region=region_name)
|
||||
self.project_id = project_id
|
||||
|
||||
def get_quota(self):
|
||||
return self.client.quotas.get(self.project_id).to_dict()
|
||||
|
||||
def get_usage(self):
|
||||
nova_usage = self.client.limits.get(
|
||||
tenant_id=self.project_id).to_dict()['absolute']
|
||||
nova_usage = self.client.limits.get(tenant_id=self.project_id).to_dict()[
|
||||
"absolute"
|
||||
]
|
||||
nova_usage_keys = [
|
||||
('instances', 'totalInstancesUsed'),
|
||||
('floating_ips', 'totalFloatingIpsUsed'),
|
||||
('ram', 'totalRAMUsed'),
|
||||
('cores', 'totalCoresUsed'),
|
||||
('secuirty_groups', 'totalSecurityGroupsUsed')
|
||||
("instances", "totalInstancesUsed"),
|
||||
("floating_ips", "totalFloatingIpsUsed"),
|
||||
("ram", "totalRAMUsed"),
|
||||
("cores", "totalCoresUsed"),
|
||||
("secuirty_groups", "totalSecurityGroupsUsed"),
|
||||
]
|
||||
|
||||
nova_usage_dict = {}
|
||||
@ -82,53 +82,48 @@ class QuotaManager(object):
|
||||
|
||||
class ServiceQuotaNeutronHelper(ServiceQuotaHelper):
|
||||
def __init__(self, region_name, project_id):
|
||||
self.client = openstack_clients.get_neutronclient(
|
||||
region=region_name)
|
||||
self.client = openstack_clients.get_neutronclient(region=region_name)
|
||||
self.project_id = project_id
|
||||
|
||||
def set_quota(self, values):
|
||||
body = {
|
||||
'quota': values
|
||||
}
|
||||
body = {"quota": values}
|
||||
self.client.update_quota(self.project_id, body)
|
||||
|
||||
def get_usage(self):
|
||||
networks = self.client.list_networks(
|
||||
tenant_id=self.project_id)['networks']
|
||||
routers = self.client.list_routers(
|
||||
tenant_id=self.project_id)['routers']
|
||||
floatingips = self.client.list_floatingips(
|
||||
tenant_id=self.project_id)['floatingips']
|
||||
ports = self.client.list_ports(
|
||||
tenant_id=self.project_id)['ports']
|
||||
subnets = self.client.list_subnets(
|
||||
tenant_id=self.project_id)['subnets']
|
||||
networks = self.client.list_networks(tenant_id=self.project_id)["networks"]
|
||||
routers = self.client.list_routers(tenant_id=self.project_id)["routers"]
|
||||
floatingips = self.client.list_floatingips(tenant_id=self.project_id)[
|
||||
"floatingips"
|
||||
]
|
||||
ports = self.client.list_ports(tenant_id=self.project_id)["ports"]
|
||||
subnets = self.client.list_subnets(tenant_id=self.project_id)["subnets"]
|
||||
security_groups = self.client.list_security_groups(
|
||||
tenant_id=self.project_id)['security_groups']
|
||||
tenant_id=self.project_id
|
||||
)["security_groups"]
|
||||
security_group_rules = self.client.list_security_group_rules(
|
||||
tenant_id=self.project_id)['security_group_rules']
|
||||
tenant_id=self.project_id
|
||||
)["security_group_rules"]
|
||||
|
||||
return {'network': len(networks),
|
||||
'router': len(routers),
|
||||
'floatingip': len(floatingips),
|
||||
'port': len(ports),
|
||||
'subnet': len(subnets),
|
||||
'secuirty_group': len(security_groups),
|
||||
'security_group_rule': len(security_group_rules)
|
||||
}
|
||||
return {
|
||||
"network": len(networks),
|
||||
"router": len(routers),
|
||||
"floatingip": len(floatingips),
|
||||
"port": len(ports),
|
||||
"subnet": len(subnets),
|
||||
"secuirty_group": len(security_groups),
|
||||
"security_group_rule": len(security_group_rules),
|
||||
}
|
||||
|
||||
def get_quota(self):
|
||||
return self.client.show_quota(self.project_id)['quota']
|
||||
return self.client.show_quota(self.project_id)["quota"]
|
||||
|
||||
class ServiceQuotaOctaviaHelper(ServiceQuotaNeutronHelper):
|
||||
def __init__(self, region_name, project_id):
|
||||
self.client = openstack_clients.get_octaviaclient(
|
||||
region=region_name)
|
||||
self.client = openstack_clients.get_octaviaclient(region=region_name)
|
||||
self.project_id = project_id
|
||||
|
||||
def get_quota(self):
|
||||
project_quota = self.client.quota_show(
|
||||
project_id=self.project_id)
|
||||
project_quota = self.client.quota_show(project_id=self.project_id)
|
||||
|
||||
# NOTE(amelia): Instead of returning the default quota if ANY
|
||||
# of the quotas are the default, the endpoint
|
||||
@ -137,40 +132,45 @@ class QuotaManager(object):
|
||||
for name, quota in project_quota.items():
|
||||
if quota is None:
|
||||
if not default_quota:
|
||||
default_quota = self.client.quota_defaults_show()[
|
||||
'quota']
|
||||
default_quota = self.client.quota_defaults_show()["quota"]
|
||||
project_quota[name] = default_quota[name]
|
||||
|
||||
return project_quota
|
||||
|
||||
def set_quota(self, values):
|
||||
self.client.quota_set(self.project_id, json={'quota': values})
|
||||
self.client.quota_set(self.project_id, json={"quota": values})
|
||||
|
||||
def get_usage(self):
|
||||
usage = {}
|
||||
usage['load_balancer'] = len(self.client.load_balancer_list(
|
||||
project_id=self.project_id)['loadbalancers'])
|
||||
usage['listener'] = len(self.client.listener_list(
|
||||
project_id=self.project_id)['listeners'])
|
||||
usage["load_balancer"] = len(
|
||||
self.client.load_balancer_list(project_id=self.project_id)[
|
||||
"loadbalancers"
|
||||
]
|
||||
)
|
||||
usage["listener"] = len(
|
||||
self.client.listener_list(project_id=self.project_id)["listeners"]
|
||||
)
|
||||
|
||||
pools = self.client.pool_list(
|
||||
project_id=self.project_id)['pools']
|
||||
usage['pool'] = len(pools)
|
||||
pools = self.client.pool_list(project_id=self.project_id)["pools"]
|
||||
usage["pool"] = len(pools)
|
||||
|
||||
members = []
|
||||
for pool in pools:
|
||||
members += pool['members']
|
||||
members += pool["members"]
|
||||
|
||||
usage['member'] = len(members)
|
||||
usage['health_monitor'] = len(self.client.health_monitor_list(
|
||||
project_id=self.project_id)['healthmonitors'])
|
||||
usage["member"] = len(members)
|
||||
usage["health_monitor"] = len(
|
||||
self.client.health_monitor_list(project_id=self.project_id)[
|
||||
"healthmonitors"
|
||||
]
|
||||
)
|
||||
return usage
|
||||
|
||||
_quota_updaters = {
|
||||
'cinder': ServiceQuotaCinderHelper,
|
||||
'nova': ServiceQuotaNovaHelper,
|
||||
'neutron': ServiceQuotaNeutronHelper,
|
||||
'octavia': ServiceQuotaOctaviaHelper,
|
||||
"cinder": ServiceQuotaCinderHelper,
|
||||
"nova": ServiceQuotaNovaHelper,
|
||||
"neutron": ServiceQuotaNeutronHelper,
|
||||
"octavia": ServiceQuotaOctaviaHelper,
|
||||
}
|
||||
|
||||
def __init__(self, project_id, size_difference_threshold=None):
|
||||
@ -182,24 +182,23 @@ class QuotaManager(object):
|
||||
|
||||
quota_services = dict(CONF.quota.services)
|
||||
|
||||
all_regions = quota_services.pop('*', None)
|
||||
all_regions = quota_services.pop("*", None)
|
||||
if all_regions:
|
||||
self.default_helpers = {}
|
||||
for service in all_regions:
|
||||
if service in self._quota_updaters:
|
||||
self.default_helpers[service] = \
|
||||
self._quota_updaters[service]
|
||||
self.default_helpers[service] = self._quota_updaters[service]
|
||||
|
||||
for region, services in quota_services.items():
|
||||
self.helpers[region] = {}
|
||||
for service in services:
|
||||
if service in self._quota_updaters:
|
||||
self.helpers[region][service] = \
|
||||
self._quota_updaters[service]
|
||||
self.helpers[region][service] = self._quota_updaters[service]
|
||||
|
||||
self.project_id = project_id
|
||||
self.size_diff_threshold = (size_difference_threshold
|
||||
or self.default_size_diff_threshold)
|
||||
self.size_diff_threshold = (
|
||||
size_difference_threshold or self.default_size_diff_threshold
|
||||
)
|
||||
|
||||
def get_current_region_quota(self, region_id):
|
||||
current_quota = {}
|
||||
@ -239,7 +238,8 @@ class QuotaManager(object):
|
||||
match_percentages.append(0.0)
|
||||
# Calculate the average of how much it matches the setting
|
||||
difference = abs(
|
||||
(sum(match_percentages) / float(len(match_percentages))) - 1)
|
||||
(sum(match_percentages) / float(len(match_percentages))) - 1
|
||||
)
|
||||
|
||||
quota_differences[size] = difference
|
||||
|
||||
@ -253,15 +253,14 @@ class QuotaManager(object):
|
||||
|
||||
quota_differences_pruned = {}
|
||||
for size, difference in quota_differences.items():
|
||||
if (difference <= diff_threshold):
|
||||
if difference <= diff_threshold:
|
||||
quota_differences_pruned[size] = difference
|
||||
|
||||
if len(quota_differences_pruned) > 0:
|
||||
return min(
|
||||
quota_differences_pruned, key=quota_differences_pruned.get)
|
||||
return min(quota_differences_pruned, key=quota_differences_pruned.get)
|
||||
# If we don't get a match return custom which means the project will
|
||||
# need admin approval for any change
|
||||
return 'custom'
|
||||
return "custom"
|
||||
|
||||
def get_quota_change_options(self, quota_size):
|
||||
""" Get's the pre-approved quota change options for a given size """
|
||||
@ -294,14 +293,14 @@ class QuotaManager(object):
|
||||
change_options = self.get_quota_change_options(current_quota_size)
|
||||
|
||||
region_data = {
|
||||
'region': region_id,
|
||||
"region": region_id,
|
||||
"current_quota": current_quota,
|
||||
"current_quota_size": current_quota_size,
|
||||
"quota_change_options": change_options,
|
||||
}
|
||||
|
||||
if include_usage:
|
||||
region_data['current_usage'] = self.get_current_usage(region_id)
|
||||
region_data["current_usage"] = self.get_current_usage(region_id)
|
||||
|
||||
return region_data
|
||||
|
||||
@ -317,11 +316,11 @@ class QuotaManager(object):
|
||||
def set_region_quota(self, region_id, quota_dict):
|
||||
notes = []
|
||||
for service_name, values in quota_dict.items():
|
||||
updater_class = self.helpers.get(
|
||||
region_id, self.default_helpers).get(service_name)
|
||||
updater_class = self.helpers.get(region_id, self.default_helpers).get(
|
||||
service_name
|
||||
)
|
||||
if not updater_class:
|
||||
notes.append("No quota updater found for %s. Ignoring" %
|
||||
service_name)
|
||||
notes.append("No quota updater found for %s. Ignoring" % service_name)
|
||||
continue
|
||||
|
||||
service_helper = updater_class(region_id, self.project_id)
|
||||
|
@ -27,10 +27,16 @@ octavia_cache = {}
|
||||
|
||||
|
||||
class FakeProject(object):
|
||||
|
||||
def __init__(self, name, description="",
|
||||
domain_id='default', parent_id=None,
|
||||
enabled=True, is_domain=False, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
description="",
|
||||
domain_id="default",
|
||||
parent_id=None,
|
||||
enabled=True,
|
||||
is_domain=False,
|
||||
**kwargs,
|
||||
):
|
||||
self.id = uuid4().hex
|
||||
self.name = name
|
||||
self.description = description
|
||||
@ -45,10 +51,15 @@ class FakeProject(object):
|
||||
|
||||
|
||||
class FakeUser(object):
|
||||
|
||||
def __init__(self, name, password="123", domain_id='default',
|
||||
enabled=True, default_project_id=None,
|
||||
**kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
password="123",
|
||||
domain_id="default",
|
||||
enabled=True,
|
||||
default_project_id=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.id = uuid4().hex
|
||||
self.name = name
|
||||
self.password = password
|
||||
@ -62,14 +73,12 @@ class FakeUser(object):
|
||||
|
||||
|
||||
class FakeRole(object):
|
||||
|
||||
def __init__(self, name):
|
||||
self.id = uuid4().hex
|
||||
self.name = name
|
||||
|
||||
|
||||
class FakeCredential(object):
|
||||
|
||||
def __init__(self, user_id, cred_type, blob, project_id=None):
|
||||
self.id = uuid4().hex
|
||||
self.user_id = user_id
|
||||
@ -79,27 +88,28 @@ class FakeCredential(object):
|
||||
|
||||
|
||||
class FakeRoleAssignment(object):
|
||||
|
||||
def __init__(self, scope, role=None, role_name=None, user=None,
|
||||
group=None, inherited=False):
|
||||
def __init__(
|
||||
self, scope, role=None, role_name=None, user=None, group=None, inherited=False
|
||||
):
|
||||
if role:
|
||||
self.role = role
|
||||
elif role_name:
|
||||
self.role = {'name': role_name}
|
||||
self.role = {"name": role_name}
|
||||
else:
|
||||
raise AttributeError("must supply 'role' or 'role_name'.")
|
||||
self.scope = scope
|
||||
self.user = user
|
||||
self.group = group
|
||||
if inherited:
|
||||
self.scope['OS-INHERIT:inherited_to'] = "projects"
|
||||
self.scope["OS-INHERIT:inherited_to"] = "projects"
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
|
||||
def setup_identity_cache(projects=None, users=None, role_assignments=None,
|
||||
credentials=None, extra_roles=None):
|
||||
def setup_identity_cache(
|
||||
projects=None, users=None, role_assignments=None, credentials=None, extra_roles=None
|
||||
):
|
||||
if extra_roles is None:
|
||||
extra_roles = []
|
||||
if not projects:
|
||||
@ -111,15 +121,17 @@ def setup_identity_cache(projects=None, users=None, role_assignments=None,
|
||||
if not credentials:
|
||||
credentials = []
|
||||
|
||||
default_domain = FakeProject(
|
||||
name="Default", is_domain=True)
|
||||
default_domain.id = 'default'
|
||||
default_domain = FakeProject(name="Default", is_domain=True)
|
||||
default_domain.id = "default"
|
||||
|
||||
projects.append(default_domain)
|
||||
|
||||
admin_user = FakeUser(
|
||||
name="admin", password="password", email="admin@example.com",
|
||||
domain_id=default_domain.id)
|
||||
name="admin",
|
||||
password="password",
|
||||
email="admin@example.com",
|
||||
domain_id=default_domain.id,
|
||||
)
|
||||
|
||||
users.append(admin_user)
|
||||
|
||||
@ -132,34 +144,28 @@ def setup_identity_cache(projects=None, users=None, role_assignments=None,
|
||||
] + extra_roles
|
||||
|
||||
region_one = mock.Mock()
|
||||
region_one.id = 'RegionOne'
|
||||
region_one.id = "RegionOne"
|
||||
|
||||
region_two = mock.Mock()
|
||||
region_two.id = 'RegionTwo'
|
||||
region_two.id = "RegionTwo"
|
||||
|
||||
global identity_cache
|
||||
|
||||
identity_cache = {
|
||||
'users': {u.id: u for u in users},
|
||||
'new_users': [],
|
||||
'projects': {p.id: p for p in projects},
|
||||
'new_projects': [],
|
||||
'role_assignments': role_assignments,
|
||||
'new_role_assignments': [],
|
||||
'roles': {r.id: r for r in roles},
|
||||
'regions': {
|
||||
'RegionOne': region_one,
|
||||
'RegionTwo': region_two
|
||||
},
|
||||
'domains': {
|
||||
default_domain.id: default_domain,
|
||||
},
|
||||
'credentials': credentials,
|
||||
"users": {u.id: u for u in users},
|
||||
"new_users": [],
|
||||
"projects": {p.id: p for p in projects},
|
||||
"new_projects": [],
|
||||
"role_assignments": role_assignments,
|
||||
"new_role_assignments": [],
|
||||
"roles": {r.id: r for r in roles},
|
||||
"regions": {"RegionOne": region_one, "RegionTwo": region_two},
|
||||
"domains": {default_domain.id: default_domain,},
|
||||
"credentials": credentials,
|
||||
}
|
||||
|
||||
|
||||
class FakeManager(object):
|
||||
|
||||
def __init__(self):
|
||||
# TODO(adriant): decide if we want to have some function calls
|
||||
# throw errors if this is false.
|
||||
@ -192,34 +198,33 @@ class FakeManager(object):
|
||||
def find_user(self, name, domain):
|
||||
domain = self._domain_from_id(domain)
|
||||
global identity_cache
|
||||
for user in identity_cache['users'].values():
|
||||
if (user.name.lower() == name.lower()
|
||||
and user.domain_id == domain.id):
|
||||
for user in identity_cache["users"].values():
|
||||
if user.name.lower() == name.lower() and user.domain_id == domain.id:
|
||||
return user
|
||||
return None
|
||||
|
||||
def get_user(self, user_id):
|
||||
global identity_cache
|
||||
return identity_cache['users'].get(user_id, None)
|
||||
return identity_cache["users"].get(user_id, None)
|
||||
|
||||
def list_users(self, project):
|
||||
project = self._project_from_id(project)
|
||||
global identity_cache
|
||||
users = {}
|
||||
|
||||
for assignment in identity_cache['role_assignments']:
|
||||
if assignment.scope['project']['id'] == project.id:
|
||||
for assignment in identity_cache["role_assignments"]:
|
||||
if assignment.scope["project"]["id"] == project.id:
|
||||
|
||||
user = users.get(assignment.user['id'])
|
||||
user = users.get(assignment.user["id"])
|
||||
if not user:
|
||||
user = self.get_user(assignment.user['id'])
|
||||
user = self.get_user(assignment.user["id"])
|
||||
user.roles = []
|
||||
user.inherited_roles = []
|
||||
users[user.id] = user
|
||||
|
||||
r = self.find_role(assignment.role['name'])
|
||||
r = self.find_role(assignment.role["name"])
|
||||
|
||||
if assignment.scope.get('OS-INHERIT:inherited_to'):
|
||||
if assignment.scope.get("OS-INHERIT:inherited_to"):
|
||||
user.inherited_roles.append(r)
|
||||
else:
|
||||
user.roles.append(r)
|
||||
@ -233,34 +238,39 @@ class FakeManager(object):
|
||||
|
||||
while project.parent_id:
|
||||
project = self._project_from_id(project.parent_id)
|
||||
for assignment in identity_cache['role_assignments']:
|
||||
if assignment.scope['project']['id'] == project.id:
|
||||
if not assignment.scope.get('OS-INHERIT:inherited_to'):
|
||||
for assignment in identity_cache["role_assignments"]:
|
||||
if assignment.scope["project"]["id"] == project.id:
|
||||
if not assignment.scope.get("OS-INHERIT:inherited_to"):
|
||||
continue
|
||||
|
||||
user = users.get(assignment.user['id'])
|
||||
user = users.get(assignment.user["id"])
|
||||
if not user:
|
||||
user = self.get_user(assignment.user['id'])
|
||||
user = self.get_user(assignment.user["id"])
|
||||
user.roles = []
|
||||
user.inherited_roles = []
|
||||
users[user.id] = user
|
||||
|
||||
r = self.find_role(assignment.role['name'])
|
||||
r = self.find_role(assignment.role["name"])
|
||||
|
||||
user.roles.append(r)
|
||||
|
||||
return users.values()
|
||||
|
||||
def create_user(self, name, password, email, created_on,
|
||||
domain='default', default_project=None):
|
||||
def create_user(
|
||||
self, name, password, email, created_on, domain="default", default_project=None
|
||||
):
|
||||
domain = self._domain_from_id(domain)
|
||||
default_project = self._project_from_id(default_project)
|
||||
global identity_cache
|
||||
user = FakeUser(
|
||||
name=name, password=password, email=email,
|
||||
domain_id=domain.id, default_project=default_project)
|
||||
identity_cache['users'][user.id] = user
|
||||
identity_cache['new_users'].append(user)
|
||||
name=name,
|
||||
password=password,
|
||||
email=email,
|
||||
domain_id=domain.id,
|
||||
default_project=default_project,
|
||||
)
|
||||
identity_cache["users"][user.id] = user
|
||||
identity_cache["new_users"].append(user)
|
||||
return user
|
||||
|
||||
def update_user_password(self, user, password):
|
||||
@ -285,7 +295,7 @@ class FakeManager(object):
|
||||
|
||||
def find_role(self, name):
|
||||
global identity_cache
|
||||
for role in identity_cache['roles'].values():
|
||||
for role in identity_cache["roles"].values():
|
||||
if role.name == name:
|
||||
return role
|
||||
return None
|
||||
@ -297,17 +307,20 @@ class FakeManager(object):
|
||||
|
||||
roles = []
|
||||
|
||||
for assignment in identity_cache['role_assignments']:
|
||||
if (assignment.user['id'] == user.id
|
||||
and assignment.scope['project']['id'] == project.id):
|
||||
for assignment in identity_cache["role_assignments"]:
|
||||
if (
|
||||
assignment.user["id"] == user.id
|
||||
and assignment.scope["project"]["id"] == project.id
|
||||
):
|
||||
|
||||
if (assignment.scope.get('OS-INHERIT:inherited_to') and not
|
||||
inherited) or (
|
||||
inherited and not
|
||||
assignment.scope.get('OS-INHERIT:inherited_to')):
|
||||
if (
|
||||
assignment.scope.get("OS-INHERIT:inherited_to") and not inherited
|
||||
) or (
|
||||
inherited and not assignment.scope.get("OS-INHERIT:inherited_to")
|
||||
):
|
||||
continue
|
||||
|
||||
r = self.find_role(assignment.role['name'])
|
||||
r = self.find_role(assignment.role["name"])
|
||||
roles.append(r)
|
||||
|
||||
return roles
|
||||
@ -319,25 +332,21 @@ class FakeManager(object):
|
||||
user = self._user_from_id(user)
|
||||
global identity_cache
|
||||
projects = {}
|
||||
for assignment in identity_cache['role_assignments']:
|
||||
if assignment.user['id'] == user.id:
|
||||
r = self.find_role(assignment.role['name'])
|
||||
for assignment in identity_cache["role_assignments"]:
|
||||
if assignment.user["id"] == user.id:
|
||||
r = self.find_role(assignment.role["name"])
|
||||
try:
|
||||
projects[assignment.scope['project']['id']].append(r)
|
||||
projects[assignment.scope["project"]["id"]].append(r)
|
||||
except KeyError:
|
||||
projects[assignment.scope['project']['id']] = [r]
|
||||
projects[assignment.scope["project"]["id"]] = [r]
|
||||
return projects
|
||||
|
||||
def _make_role_assignment(self, user, role, project, inherited=False):
|
||||
scope = {
|
||||
'project': {
|
||||
'id': project.id}}
|
||||
scope = {"project": {"id": project.id}}
|
||||
if inherited:
|
||||
scope['OS-INHERIT:inherited_to'] = "projects"
|
||||
scope["OS-INHERIT:inherited_to"] = "projects"
|
||||
role_assignment = FakeRoleAssignment(
|
||||
scope=scope,
|
||||
role={"name": role.name},
|
||||
user={'id': user.id},
|
||||
scope=scope, role={"name": role.name}, user={"id": user.id},
|
||||
)
|
||||
return role_assignment
|
||||
|
||||
@ -350,45 +359,51 @@ class FakeManager(object):
|
||||
|
||||
global identity_cache
|
||||
|
||||
if role_assignment not in identity_cache['role_assignments']:
|
||||
identity_cache['role_assignments'].append(role_assignment)
|
||||
identity_cache['new_role_assignments'].append(role_assignment)
|
||||
if role_assignment not in identity_cache["role_assignments"]:
|
||||
identity_cache["role_assignments"].append(role_assignment)
|
||||
identity_cache["new_role_assignments"].append(role_assignment)
|
||||
|
||||
def remove_user_role(self, user, role, project, inherited=False):
|
||||
user = self._user_from_id(user)
|
||||
role = self._role_from_id(role)
|
||||
project = self._project_from_id(project)
|
||||
|
||||
role_assignment = self._make_role_assignment(user, role, project,
|
||||
inherited=inherited)
|
||||
role_assignment = self._make_role_assignment(
|
||||
user, role, project, inherited=inherited
|
||||
)
|
||||
|
||||
global identity_cache
|
||||
|
||||
if role_assignment in identity_cache['role_assignments']:
|
||||
identity_cache['role_assignments'].remove(role_assignment)
|
||||
if role_assignment in identity_cache["role_assignments"]:
|
||||
identity_cache["role_assignments"].remove(role_assignment)
|
||||
|
||||
def find_project(self, project_name, domain):
|
||||
domain = self._domain_from_id(domain)
|
||||
global identity_cache
|
||||
for project in identity_cache['projects'].values():
|
||||
if (project.name.lower() == project_name.lower()
|
||||
and project.domain_id == domain.id):
|
||||
for project in identity_cache["projects"].values():
|
||||
if (
|
||||
project.name.lower() == project_name.lower()
|
||||
and project.domain_id == domain.id
|
||||
):
|
||||
return project
|
||||
return None
|
||||
|
||||
def get_project(self, project_id, subtree_as_ids=False,
|
||||
parents_as_ids=False):
|
||||
def get_project(self, project_id, subtree_as_ids=False, parents_as_ids=False):
|
||||
global identity_cache
|
||||
project = identity_cache['projects'].get(project_id, None)
|
||||
project = identity_cache["projects"].get(project_id, None)
|
||||
|
||||
if subtree_as_ids:
|
||||
subtree_list = []
|
||||
prev_layer = [project.id, ]
|
||||
prev_layer = [
|
||||
project.id,
|
||||
]
|
||||
current_layer = True
|
||||
while current_layer:
|
||||
current_layer = [s_project.id for s_project in
|
||||
identity_cache['projects'].values()
|
||||
if project.parent_id in prev_layer]
|
||||
current_layer = [
|
||||
s_project.id
|
||||
for s_project in identity_cache["projects"].values()
|
||||
if project.parent_id in prev_layer
|
||||
]
|
||||
prev_layer = current_layer
|
||||
subtree_list.append(current_layer)
|
||||
|
||||
@ -398,8 +413,8 @@ class FakeManager(object):
|
||||
parent_list = []
|
||||
parent_id = project.parent_id
|
||||
parent_list.append(parent_id)
|
||||
while identity_cache['projects'].get(parent_id, None):
|
||||
parent_id = identity_cache['projects'].get(parent_id, None)
|
||||
while identity_cache["projects"].get(parent_id, None):
|
||||
parent_id = identity_cache["projects"].get(parent_id, None)
|
||||
parent_list.append(parent_id)
|
||||
|
||||
project.parent_ids = parent_list
|
||||
@ -408,20 +423,23 @@ class FakeManager(object):
|
||||
|
||||
return project
|
||||
|
||||
def create_project(self, project_name, created_on, parent=None,
|
||||
domain='default', description=""):
|
||||
def create_project(
|
||||
self, project_name, created_on, parent=None, domain="default", description=""
|
||||
):
|
||||
parent = self._project_from_id(parent)
|
||||
domain = self._domain_from_id(domain)
|
||||
global identity_cache
|
||||
|
||||
project = FakeProject(
|
||||
name=project_name, created_on=created_on, description=description,
|
||||
domain_id=domain.id
|
||||
name=project_name,
|
||||
created_on=created_on,
|
||||
description=description,
|
||||
domain_id=domain.id,
|
||||
)
|
||||
if parent:
|
||||
project.parent_id = parent.id
|
||||
identity_cache['projects'][project.id] = project
|
||||
identity_cache['new_projects'].append(project)
|
||||
identity_cache["projects"][project.id] = project
|
||||
identity_cache["new_projects"].append(project)
|
||||
return project
|
||||
|
||||
def update_project(self, project, **kwargs):
|
||||
@ -433,27 +451,27 @@ class FakeManager(object):
|
||||
|
||||
def find_domain(self, domain_name):
|
||||
global identity_cache
|
||||
for domain in identity_cache['domains'].values():
|
||||
for domain in identity_cache["domains"].values():
|
||||
if domain.name.lower() == domain_name.lower():
|
||||
return domain
|
||||
return None
|
||||
|
||||
def get_domain(self, domain_id):
|
||||
global identity_cache
|
||||
return identity_cache['domains'].get(domain_id, None)
|
||||
return identity_cache["domains"].get(domain_id, None)
|
||||
|
||||
def get_region(self, region_id):
|
||||
global identity_cache
|
||||
return identity_cache['regions'].get(region_id, None)
|
||||
return identity_cache["regions"].get(region_id, None)
|
||||
|
||||
def list_regions(self):
|
||||
global identity_cache
|
||||
return identity_cache['regions'].values()
|
||||
return identity_cache["regions"].values()
|
||||
|
||||
def list_credentials(self, user_id, cred_type=None):
|
||||
global identity_cache
|
||||
found = []
|
||||
for cred in identity_cache['credentials']:
|
||||
for cred in identity_cache["credentials"]:
|
||||
if cred.user_id == user_id:
|
||||
if cred_type and cred.type == cred_type:
|
||||
found.append(cred)
|
||||
@ -465,21 +483,20 @@ class FakeManager(object):
|
||||
global identity_cache
|
||||
user = self._user_from_id(user)
|
||||
project = self._project_from_id(project)
|
||||
cred = FakeCredential(
|
||||
user_id=user.id, blob=blob, cred_type=cred_type)
|
||||
cred = FakeCredential(user_id=user.id, blob=blob, cred_type=cred_type)
|
||||
if project:
|
||||
cred.project_id = project.id
|
||||
identity_cache['credentials'].append(cred)
|
||||
identity_cache["credentials"].append(cred)
|
||||
return cred
|
||||
|
||||
def clear_credential_type(self, user_id, cred_type):
|
||||
global identity_cache
|
||||
found = []
|
||||
for cred in identity_cache['credentials']:
|
||||
for cred in identity_cache["credentials"]:
|
||||
if cred.user_id == user_id and cred.type == cred_type:
|
||||
found.append(cred)
|
||||
for cred in found:
|
||||
identity_cache['credentials'].remove(cred)
|
||||
identity_cache["credentials"].remove(cred)
|
||||
|
||||
# TODO(adriant): Move this to a BaseIdentityManager class when
|
||||
# it exists.
|
||||
@ -499,9 +516,12 @@ class FakeManager(object):
|
||||
return list(set(all_roles))
|
||||
|
||||
# merge mapping lists to form a flat permitted roles list
|
||||
manageable_role_names = [mrole for role_name in user_roles
|
||||
if role_name in roles_mapping
|
||||
for mrole in roles_mapping[role_name]]
|
||||
manageable_role_names = [
|
||||
mrole
|
||||
for role_name in user_roles
|
||||
if role_name in roles_mapping
|
||||
for mrole in roles_mapping[role_name]
|
||||
]
|
||||
# a set has unique items
|
||||
manageable_role_names = set(manageable_role_names)
|
||||
return manageable_role_names
|
||||
@ -510,6 +530,7 @@ class FakeManager(object):
|
||||
class FakeOpenstackClient(object):
|
||||
class Quotas(object):
|
||||
""" Stub class for testing quotas """
|
||||
|
||||
def __init__(self, service):
|
||||
self.service = service
|
||||
|
||||
@ -518,7 +539,8 @@ class FakeOpenstackClient(object):
|
||||
|
||||
def get(self, project_id):
|
||||
return self.QuotaSet(
|
||||
self.service._cache[self.service.region][project_id]['quota'])
|
||||
self.service._cache[self.service.region][project_id]["quota"]
|
||||
)
|
||||
|
||||
class QuotaSet(object):
|
||||
def __init__(self, data):
|
||||
@ -536,58 +558,66 @@ class FakeOpenstackClient(object):
|
||||
if self.region not in self._cache:
|
||||
self._cache[self.region] = {}
|
||||
if project_id not in self._cache[self.region]:
|
||||
self._cache[self.region][project_id] = {
|
||||
'quota': {}
|
||||
}
|
||||
quota = self._cache[self.region][project_id]['quota']
|
||||
self._cache[self.region][project_id] = {"quota": {}}
|
||||
quota = self._cache[self.region][project_id]["quota"]
|
||||
quota.update(kwargs)
|
||||
|
||||
|
||||
class FakeNeutronClient(object):
|
||||
|
||||
def __init__(self, region):
|
||||
self.region = region
|
||||
|
||||
def create_network(self, body):
|
||||
global neutron_cache
|
||||
project_id = body['network']['tenant_id']
|
||||
net = {'network': {'id': 'net_id_%s' % neutron_cache['RegionOne']['i'],
|
||||
'body': body}}
|
||||
net_id = net['network']['id']
|
||||
neutron_cache['RegionOne'][project_id]['networks'][net_id] = net
|
||||
neutron_cache['RegionOne']['i'] += 1
|
||||
project_id = body["network"]["tenant_id"]
|
||||
net = {
|
||||
"network": {
|
||||
"id": "net_id_%s" % neutron_cache["RegionOne"]["i"],
|
||||
"body": body,
|
||||
}
|
||||
}
|
||||
net_id = net["network"]["id"]
|
||||
neutron_cache["RegionOne"][project_id]["networks"][net_id] = net
|
||||
neutron_cache["RegionOne"]["i"] += 1
|
||||
return net
|
||||
|
||||
def create_subnet(self, body):
|
||||
global neutron_cache
|
||||
project_id = body['subnet']['tenant_id']
|
||||
subnet = {'subnet': {'id': 'subnet_id_%s'
|
||||
% neutron_cache['RegionOne']['i'],
|
||||
'body': body}}
|
||||
sub_id = subnet['subnet']['id']
|
||||
neutron_cache['RegionOne'][project_id]['subnets'][sub_id] = subnet
|
||||
neutron_cache['RegionOne']['i'] += 1
|
||||
project_id = body["subnet"]["tenant_id"]
|
||||
subnet = {
|
||||
"subnet": {
|
||||
"id": "subnet_id_%s" % neutron_cache["RegionOne"]["i"],
|
||||
"body": body,
|
||||
}
|
||||
}
|
||||
sub_id = subnet["subnet"]["id"]
|
||||
neutron_cache["RegionOne"][project_id]["subnets"][sub_id] = subnet
|
||||
neutron_cache["RegionOne"]["i"] += 1
|
||||
return subnet
|
||||
|
||||
def create_router(self, body):
|
||||
global neutron_cache
|
||||
project_id = body['router']['tenant_id']
|
||||
router = {'router': {'id': 'router_id_%s'
|
||||
% neutron_cache['RegionOne']['i'],
|
||||
'body': body}}
|
||||
router_id = router['router']['id']
|
||||
neutron_cache['RegionOne'][project_id]['routers'][router_id] = router
|
||||
neutron_cache['RegionOne']['i'] += 1
|
||||
project_id = body["router"]["tenant_id"]
|
||||
router = {
|
||||
"router": {
|
||||
"id": "router_id_%s" % neutron_cache["RegionOne"]["i"],
|
||||
"body": body,
|
||||
}
|
||||
}
|
||||
router_id = router["router"]["id"]
|
||||
neutron_cache["RegionOne"][project_id]["routers"][router_id] = router
|
||||
neutron_cache["RegionOne"]["i"] += 1
|
||||
return router
|
||||
|
||||
def add_interface_router(self, router_id, body):
|
||||
global neutron_cache
|
||||
port_id = "port_id_%s" % neutron_cache['RegionOne']['i']
|
||||
neutron_cache['RegionOne']['i'] += 1
|
||||
port_id = "port_id_%s" % neutron_cache["RegionOne"]["i"]
|
||||
neutron_cache["RegionOne"]["i"] += 1
|
||||
interface = {
|
||||
'port_id': port_id,
|
||||
'id': router_id,
|
||||
'subnet_id': body['subnet_id']}
|
||||
"port_id": port_id,
|
||||
"id": router_id,
|
||||
"subnet_id": body["subnet_id"],
|
||||
}
|
||||
return interface
|
||||
|
||||
def update_quota(self, project_id, body):
|
||||
@ -597,14 +627,14 @@ class FakeNeutronClient(object):
|
||||
if project_id not in neutron_cache[self.region]:
|
||||
neutron_cache[self.region][project_id] = {}
|
||||
|
||||
if 'quota' not in neutron_cache[self.region][project_id]:
|
||||
neutron_cache[self.region][project_id]['quota'] = {}
|
||||
if "quota" not in neutron_cache[self.region][project_id]:
|
||||
neutron_cache[self.region][project_id]["quota"] = {}
|
||||
|
||||
quota = neutron_cache[self.region][project_id]['quota']
|
||||
quota.update(body['quota'])
|
||||
quota = neutron_cache[self.region][project_id]["quota"]
|
||||
quota.update(body["quota"])
|
||||
|
||||
def show_quota(self, project_id):
|
||||
return {"quota": neutron_cache[self.region][project_id]['quota']}
|
||||
return {"quota": neutron_cache[self.region][project_id]["quota"]}
|
||||
|
||||
def list_networks(self, tenant_id):
|
||||
return neutron_cache[self.region][tenant_id]
|
||||
@ -630,11 +660,13 @@ class FakeNeutronClient(object):
|
||||
|
||||
class FakeOctaviaClient(object):
|
||||
# {name in client call: name in response}
|
||||
resource_dict = {'load_balancer': 'loadbalancers',
|
||||
'listener': 'listeners',
|
||||
'member': 'members',
|
||||
'pool': 'pools',
|
||||
'health_monitor': 'healthmonitors'}
|
||||
resource_dict = {
|
||||
"load_balancer": "loadbalancers",
|
||||
"listener": "listeners",
|
||||
"member": "members",
|
||||
"pool": "pools",
|
||||
"health_monitor": "healthmonitors",
|
||||
}
|
||||
|
||||
# NOTE(amelia): Using the current octavia client we will get back
|
||||
# dicts for everything, rather than the resources the
|
||||
@ -651,15 +683,15 @@ class FakeOctaviaClient(object):
|
||||
|
||||
def quota_show(self, project_id):
|
||||
self._ensure_project_exists(project_id)
|
||||
quota = self.cache.get(project_id, {}).get('quota', [])
|
||||
quota = self.cache.get(project_id, {}).get("quota", [])
|
||||
for item in self.resource_dict:
|
||||
if item not in quota:
|
||||
quota[item] = None
|
||||
return {'quota': quota}
|
||||
return {"quota": quota}
|
||||
|
||||
def quota_set(self, project_id, json):
|
||||
self._ensure_project_exists(project_id)
|
||||
self.cache[project_id]['quota'] = json['quota']
|
||||
self.cache[project_id]["quota"] = json["quota"]
|
||||
|
||||
def quota_defaults_show(self):
|
||||
return {
|
||||
@ -668,7 +700,7 @@ class FakeOctaviaClient(object):
|
||||
"listener": -1,
|
||||
"member": 50,
|
||||
"pool": -1,
|
||||
"health_monitor": -1
|
||||
"health_monitor": -1,
|
||||
}
|
||||
}
|
||||
|
||||
@ -676,29 +708,27 @@ class FakeOctaviaClient(object):
|
||||
def action(project_id=None):
|
||||
self._ensure_project_exists(project_id)
|
||||
resource = self.cache.get(project_id, {}).get(resource_type, [])
|
||||
links_name = resource_type + '_links'
|
||||
links_name = resource_type + "_links"
|
||||
resource_name = self.resource_dict[resource_type]
|
||||
return {resource_name: resource, links_name: []}
|
||||
|
||||
return action
|
||||
|
||||
def _ensure_project_exists(self, project_id):
|
||||
if project_id not in self.cache:
|
||||
self.cache[project_id] = {
|
||||
name: [] for name in self.resource_dict.keys()}
|
||||
self.cache[project_id]['quota'] = dict(
|
||||
CONF.quota.sizes['small']['octavia'])
|
||||
self.cache[project_id] = {name: [] for name in self.resource_dict.keys()}
|
||||
self.cache[project_id]["quota"] = dict(CONF.quota.sizes["small"]["octavia"])
|
||||
|
||||
def __getattr__(self, name):
|
||||
# NOTE(amelia): This is out of pure laziness
|
||||
global octavia_cache
|
||||
if name[-5:] == '_list' and name[:-5] in self.resource_dict:
|
||||
if name[-5:] == "_list" and name[:-5] in self.resource_dict:
|
||||
return self.lister(name[:-5])
|
||||
else:
|
||||
raise AttributeError
|
||||
|
||||
|
||||
class FakeNovaClient(FakeOpenstackClient):
|
||||
|
||||
def __init__(self, region):
|
||||
global nova_cache
|
||||
super(FakeNovaClient, self).__init__(region, nova_cache)
|
||||
@ -730,7 +760,7 @@ class FakeCinderClient(FakeOpenstackClient):
|
||||
|
||||
def list(self, search_opts=None):
|
||||
if search_opts:
|
||||
project_id = search_opts['project_id']
|
||||
project_id = search_opts["project_id"]
|
||||
global cinder_cache
|
||||
return cinder_cache[self.region][project_id][self.key]
|
||||
|
||||
@ -739,9 +769,8 @@ class FakeCinderClient(FakeOpenstackClient):
|
||||
self.region = region
|
||||
self._cache = cinder_cache
|
||||
self.quotas = FakeOpenstackClient.Quotas(self)
|
||||
self.volumes = self.FakeResourceGroup(region, 'volumes')
|
||||
self.volume_snapshots = self.FakeResourceGroup(region,
|
||||
'volume_snapshots')
|
||||
self.volumes = self.FakeResourceGroup(region, "volumes")
|
||||
self.volume_snapshots = self.FakeResourceGroup(region, "volume_snapshots")
|
||||
|
||||
|
||||
class FakeResource(object):
|
||||
@ -755,24 +784,25 @@ class FakeResource(object):
|
||||
def setup_neutron_cache(region, project_id):
|
||||
global neutron_cache
|
||||
if region not in neutron_cache:
|
||||
neutron_cache[region] = {'i': 0}
|
||||
neutron_cache[region] = {"i": 0}
|
||||
else:
|
||||
neutron_cache[region]['i'] = 0
|
||||
neutron_cache[region]["i"] = 0
|
||||
if project_id not in neutron_cache[region]:
|
||||
neutron_cache[region][project_id] = {}
|
||||
|
||||
neutron_cache[region][project_id] = {
|
||||
'networks': {},
|
||||
'subnets': {},
|
||||
'routers': {},
|
||||
'security_groups': {},
|
||||
'floatingips': {},
|
||||
'security_group_rules': {},
|
||||
'ports': {},
|
||||
"networks": {},
|
||||
"subnets": {},
|
||||
"routers": {},
|
||||
"security_groups": {},
|
||||
"floatingips": {},
|
||||
"security_group_rules": {},
|
||||
"ports": {},
|
||||
}
|
||||
|
||||
neutron_cache[region][project_id]['quota'] = dict(
|
||||
CONF.quota.sizes['small']['neutron'])
|
||||
neutron_cache[region][project_id]["quota"] = dict(
|
||||
CONF.quota.sizes["small"]["neutron"]
|
||||
)
|
||||
|
||||
|
||||
def setup_cinder_cache(region, project_id):
|
||||
@ -783,12 +813,13 @@ def setup_cinder_cache(region, project_id):
|
||||
cinder_cache[region][project_id] = {}
|
||||
|
||||
cinder_cache[region][project_id] = {
|
||||
'volumes': [],
|
||||
'volume_snapshots': [],
|
||||
"volumes": [],
|
||||
"volume_snapshots": [],
|
||||
}
|
||||
|
||||
cinder_cache[region][project_id]['quota'] = dict(
|
||||
CONF.quota.sizes['small']['cinder'])
|
||||
cinder_cache[region][project_id]["quota"] = dict(
|
||||
CONF.quota.sizes["small"]["cinder"]
|
||||
)
|
||||
|
||||
|
||||
def setup_nova_cache(region, project_id):
|
||||
@ -800,19 +831,18 @@ def setup_nova_cache(region, project_id):
|
||||
|
||||
# Mocking the nova limits api
|
||||
nova_cache[region][project_id] = {
|
||||
'absolute': {
|
||||
"absolute": {
|
||||
"totalInstancesUsed": 0,
|
||||
"totalFloatingIpsUsed": 0,
|
||||
"totalRAMUsed": 0,
|
||||
"totalCoresUsed": 0,
|
||||
"totalSecurityGroupsUsed": 0
|
||||
"totalSecurityGroupsUsed": 0,
|
||||
}
|
||||
}
|
||||
nova_cache[region][project_id]['quota'] = dict(
|
||||
CONF.quota.sizes['small']['nova'])
|
||||
nova_cache[region][project_id]["quota"] = dict(CONF.quota.sizes["small"]["nova"])
|
||||
|
||||
|
||||
def setup_quota_cache(region_name, project_id, size='small'):
|
||||
def setup_quota_cache(region_name, project_id, size="small"):
|
||||
""" Sets up the quota cache for a given region and project """
|
||||
global cinder_cache
|
||||
|
||||
@ -820,36 +850,31 @@ def setup_quota_cache(region_name, project_id, size='small'):
|
||||
cinder_cache[region_name] = {}
|
||||
|
||||
if project_id not in cinder_cache[region_name]:
|
||||
cinder_cache[region_name][project_id] = {
|
||||
'quota': {}
|
||||
}
|
||||
cinder_cache[region_name][project_id] = {"quota": {}}
|
||||
|
||||
cinder_cache[region_name][project_id]['quota'] = dict(
|
||||
CONF.quota.sizes[size]['cinder'])
|
||||
cinder_cache[region_name][project_id]["quota"] = dict(
|
||||
CONF.quota.sizes[size]["cinder"]
|
||||
)
|
||||
|
||||
global nova_cache
|
||||
if region_name not in nova_cache:
|
||||
nova_cache[region_name] = {}
|
||||
|
||||
if project_id not in nova_cache[region_name]:
|
||||
nova_cache[region_name][project_id] = {
|
||||
'quota': {}
|
||||
}
|
||||
nova_cache[region_name][project_id] = {"quota": {}}
|
||||
|
||||
nova_cache[region_name][project_id]['quota'] = dict(
|
||||
CONF.quota.sizes[size]['nova'])
|
||||
nova_cache[region_name][project_id]["quota"] = dict(CONF.quota.sizes[size]["nova"])
|
||||
|
||||
global neutron_cache
|
||||
if region_name not in neutron_cache:
|
||||
neutron_cache[region_name] = {}
|
||||
|
||||
if project_id not in neutron_cache[region_name]:
|
||||
neutron_cache[region_name][project_id] = {
|
||||
'quota': {}
|
||||
}
|
||||
neutron_cache[region_name][project_id] = {"quota": {}}
|
||||
|
||||
neutron_cache[region_name][project_id]['quota'] = dict(
|
||||
CONF.quota.sizes[size]['neutron'])
|
||||
neutron_cache[region_name][project_id]["quota"] = dict(
|
||||
CONF.quota.sizes[size]["neutron"]
|
||||
)
|
||||
|
||||
|
||||
def setup_mock_caches(region, project_id):
|
||||
|
@ -19,7 +19,6 @@ from adjutant.common.tests import fake_clients
|
||||
|
||||
|
||||
class AdjutantTestCase(TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
fake_clients.identity_cache.clear()
|
||||
fake_clients.neutron_cache.clear()
|
||||
@ -28,7 +27,6 @@ class AdjutantTestCase(TestCase):
|
||||
|
||||
|
||||
class AdjutantAPITestCase(APITestCase):
|
||||
|
||||
def tearDown(self):
|
||||
fake_clients.identity_cache.clear()
|
||||
fake_clients.neutron_cache.clear()
|
||||
|
@ -82,23 +82,20 @@ class IdentityManager(object): # pragma: no cover
|
||||
|
||||
users = {}
|
||||
|
||||
user_assignments = self.ks_client.role_assignments.list(
|
||||
project=project)
|
||||
user_assignments = self.ks_client.role_assignments.list(project=project)
|
||||
for assignment in user_assignments:
|
||||
try:
|
||||
user = users.get(assignment.user['id'], None)
|
||||
user = users.get(assignment.user["id"], None)
|
||||
if not user:
|
||||
user = self.ks_client.users.get(
|
||||
assignment.user['id'])
|
||||
user = self.ks_client.users.get(assignment.user["id"])
|
||||
user.roles = []
|
||||
user.inherited_roles = []
|
||||
users[user.id] = user
|
||||
|
||||
if assignment.scope.get('OS-INHERIT:inherited_to'):
|
||||
user.inherited_roles.append(
|
||||
role_dict[assignment.role['id']])
|
||||
if assignment.scope.get("OS-INHERIT:inherited_to"):
|
||||
user.inherited_roles.append(role_dict[assignment.role["id"]])
|
||||
else:
|
||||
user.roles.append(role_dict[assignment.role['id']])
|
||||
user.roles.append(role_dict[assignment.role["id"]])
|
||||
except AttributeError:
|
||||
# Just means the assignment is a group, so ignore it.
|
||||
pass
|
||||
@ -119,22 +116,19 @@ class IdentityManager(object): # pragma: no cover
|
||||
project = self.ks_client.projects.get(project)
|
||||
while project.parent_id:
|
||||
project = self.ks_client.projects.get(project.parent_id)
|
||||
user_assignments = self.ks_client.role_assignments.list(
|
||||
project=project)
|
||||
user_assignments = self.ks_client.role_assignments.list(project=project)
|
||||
for assignment in user_assignments:
|
||||
if not assignment.scope.get('OS-INHERIT:inherited_to'):
|
||||
if not assignment.scope.get("OS-INHERIT:inherited_to"):
|
||||
continue
|
||||
try:
|
||||
user = users.get(
|
||||
assignment.user['id'], None)
|
||||
user = users.get(assignment.user["id"], None)
|
||||
if user:
|
||||
user.roles.append(
|
||||
role_dict[assignment.role['id']])
|
||||
user.roles.append(role_dict[assignment.role["id"]])
|
||||
else:
|
||||
user = self.ks_client.users.get(
|
||||
assignment.user['id'])
|
||||
user = self.ks_client.users.get(assignment.user["id"])
|
||||
user.roles = [
|
||||
role_dict[assignment.role['id']], ]
|
||||
role_dict[assignment.role["id"]],
|
||||
]
|
||||
user.inherited_roles = []
|
||||
users[user.id] = user
|
||||
except AttributeError:
|
||||
@ -146,12 +140,18 @@ class IdentityManager(object): # pragma: no cover
|
||||
return []
|
||||
return users.values()
|
||||
|
||||
def create_user(self, name, password, email, created_on, domain=None,
|
||||
default_project=None):
|
||||
def create_user(
|
||||
self, name, password, email, created_on, domain=None, default_project=None
|
||||
):
|
||||
|
||||
user = self.ks_client.users.create(
|
||||
name=name, password=password, domain=domain, email=email,
|
||||
default_project=default_project, created_on=created_on)
|
||||
name=name,
|
||||
password=password,
|
||||
domain=domain,
|
||||
email=email,
|
||||
default_project=default_project,
|
||||
created_on=created_on,
|
||||
)
|
||||
return user
|
||||
|
||||
def enable_user(self, user):
|
||||
@ -182,14 +182,14 @@ class IdentityManager(object): # pragma: no cover
|
||||
|
||||
user_roles = []
|
||||
user_assignments = self.ks_client.role_assignments.list(
|
||||
user=user, project=project)
|
||||
user=user, project=project
|
||||
)
|
||||
for assignment in user_assignments:
|
||||
if (assignment.scope.get('OS-INHERIT:inherited_to') and not
|
||||
inherited) or (
|
||||
inherited and not
|
||||
assignment.scope.get('OS-INHERIT:inherited_to')):
|
||||
if (assignment.scope.get("OS-INHERIT:inherited_to") and not inherited) or (
|
||||
inherited and not assignment.scope.get("OS-INHERIT:inherited_to")
|
||||
):
|
||||
continue
|
||||
user_roles.append(role_dict[assignment.role['id']])
|
||||
user_roles.append(role_dict[assignment.role["id"]])
|
||||
return user_roles
|
||||
|
||||
def get_all_roles(self, user):
|
||||
@ -204,8 +204,8 @@ class IdentityManager(object): # pragma: no cover
|
||||
user_assignments = self.ks_client.role_assignments.list(user=user)
|
||||
projects = defaultdict(list)
|
||||
for assignment in user_assignments:
|
||||
project = assignment.scope['project']['id']
|
||||
projects[project].append(role_dict[assignment.role['id']])
|
||||
project = assignment.scope["project"]["id"]
|
||||
projects[project].append(role_dict[assignment.role["id"]])
|
||||
|
||||
return projects
|
||||
|
||||
@ -213,8 +213,11 @@ class IdentityManager(object): # pragma: no cover
|
||||
try:
|
||||
if inherited:
|
||||
self.ks_client.roles.grant(
|
||||
role, user=user, project=project,
|
||||
os_inherit_extension_inherited=inherited)
|
||||
role,
|
||||
user=user,
|
||||
project=project,
|
||||
os_inherit_extension_inherited=inherited,
|
||||
)
|
||||
else:
|
||||
self.ks_client.roles.grant(role, user=user, project=project)
|
||||
except ks_exceptions.Conflict:
|
||||
@ -224,8 +227,11 @@ class IdentityManager(object): # pragma: no cover
|
||||
def remove_user_role(self, user, role, project, inherited=False):
|
||||
if inherited:
|
||||
self.ks_client.roles.revoke(
|
||||
role, user=user, project=project,
|
||||
os_inherit_extension_inherited=inherited)
|
||||
role,
|
||||
user=user,
|
||||
project=project,
|
||||
os_inherit_extension_inherited=inherited,
|
||||
)
|
||||
else:
|
||||
self.ks_client.roles.revoke(role, user=user, project=project)
|
||||
|
||||
@ -233,8 +239,7 @@ class IdentityManager(object): # pragma: no cover
|
||||
try:
|
||||
# Using a filtered list as find is more efficient than
|
||||
# using the client find
|
||||
projects = self.ks_client.projects.list(
|
||||
name=project_name, domain=domain)
|
||||
projects = self.ks_client.projects.list(name=project_name, domain=domain)
|
||||
if projects:
|
||||
# NOTE(adriant) project names are unique in a domain so
|
||||
# it is safe to assume filtering on project name and domain
|
||||
@ -245,12 +250,11 @@ class IdentityManager(object): # pragma: no cover
|
||||
except ks_exceptions.NotFound:
|
||||
return None
|
||||
|
||||
def get_project(self, project_id, subtree_as_ids=False,
|
||||
parents_as_ids=False):
|
||||
def get_project(self, project_id, subtree_as_ids=False, parents_as_ids=False):
|
||||
try:
|
||||
project = self.ks_client.projects.get(
|
||||
project_id, subtree_as_ids=subtree_as_ids,
|
||||
parents_as_ids=parents_as_ids)
|
||||
project_id, subtree_as_ids=subtree_as_ids, parents_as_ids=parents_as_ids
|
||||
)
|
||||
if parents_as_ids:
|
||||
depth = 1
|
||||
last_root = None
|
||||
@ -276,21 +280,31 @@ class IdentityManager(object): # pragma: no cover
|
||||
except ks_exceptions.NotFound:
|
||||
return []
|
||||
|
||||
def update_project(self, project, name=None, domain=None, description=None,
|
||||
enabled=None, **kwargs):
|
||||
def update_project(
|
||||
self, project, name=None, domain=None, description=None, enabled=None, **kwargs
|
||||
):
|
||||
try:
|
||||
return self.ks_client.projects.update(
|
||||
project=project, domain=domain, name=name,
|
||||
description=description, enabled=enabled,
|
||||
**kwargs)
|
||||
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=None, description=""):
|
||||
def create_project(
|
||||
self, project_name, created_on, parent=None, domain=None, description=""
|
||||
):
|
||||
project = self.ks_client.projects.create(
|
||||
project_name, domain, parent=parent, created_on=created_on,
|
||||
description=description)
|
||||
project_name,
|
||||
domain,
|
||||
parent=parent,
|
||||
created_on=created_on,
|
||||
description=description,
|
||||
)
|
||||
return project
|
||||
|
||||
def get_domain(self, domain_id):
|
||||
@ -321,20 +335,19 @@ class IdentityManager(object): # pragma: no cover
|
||||
return self.ks_client.regions.list(**kwargs)
|
||||
|
||||
def list_credentials(self, user_id, cred_type=None):
|
||||
return self.ks_client.credentials.list(
|
||||
user_id=user_id, type=cred_type)
|
||||
return self.ks_client.credentials.list(user_id=user_id, type=cred_type)
|
||||
|
||||
def add_credential(self, user, cred_type, blob, project=None):
|
||||
return self.ks_client.credentials.create(
|
||||
user=user, type=cred_type, blob=blob, project=project)
|
||||
user=user, type=cred_type, blob=blob, project=project
|
||||
)
|
||||
|
||||
def delete_credential(self, credential):
|
||||
return self.ks_client.credentials.delete(credential)
|
||||
|
||||
def clear_credential_type(self, user_id, cred_type):
|
||||
# list credentials of the type for the user
|
||||
credentials = self.ks_client.credentials.list(
|
||||
user_id=user_id, type=cred_type)
|
||||
credentials = self.ks_client.credentials.list(user_id=user_id, type=cred_type)
|
||||
for cred in credentials:
|
||||
if cred.user_id == user_id and cred.type == cred_type:
|
||||
self.ks_client.credentials.delete(cred)
|
||||
@ -357,9 +370,12 @@ class IdentityManager(object): # pragma: no cover
|
||||
return list(set(all_roles))
|
||||
|
||||
# merge mapping lists to form a flat permitted roles list
|
||||
manageable_role_names = [mrole for role_name in user_roles
|
||||
if role_name in roles_mapping
|
||||
for mrole in roles_mapping[role_name]]
|
||||
manageable_role_names = [
|
||||
mrole
|
||||
for role_name in user_roles
|
||||
if role_name in roles_mapping
|
||||
for mrole in roles_mapping[role_name]
|
||||
]
|
||||
# a set has unique items
|
||||
manageable_role_names = set(manageable_role_names)
|
||||
return manageable_role_names
|
||||
|
@ -40,13 +40,13 @@ _old_config_file = "/etc/adjutant/conf.yaml"
|
||||
|
||||
_test_mode_commands = [
|
||||
# Adjutant commands:
|
||||
'exampleconfig',
|
||||
"exampleconfig",
|
||||
# Django commands:
|
||||
'check',
|
||||
'makemigrations',
|
||||
'squashmigrations',
|
||||
'test',
|
||||
'testserver',
|
||||
"check",
|
||||
"makemigrations",
|
||||
"squashmigrations",
|
||||
"test",
|
||||
"testserver",
|
||||
]
|
||||
|
||||
|
||||
@ -81,7 +81,11 @@ def _load_config():
|
||||
% conf_file_loc
|
||||
)
|
||||
|
||||
if used_config_loc != conf_file and used_config_loc == _old_config_file and not test_mode:
|
||||
if (
|
||||
used_config_loc != conf_file
|
||||
and used_config_loc == _old_config_file
|
||||
and not test_mode
|
||||
):
|
||||
print(
|
||||
"DEPRECATED: Using the old default config location '%s' is deprecated "
|
||||
"in favor of '%s', or setting a config location via the environment "
|
||||
|
@ -24,26 +24,26 @@ config_group.register_child_config(
|
||||
help_text="List of Active Delegate APIs.",
|
||||
required=True,
|
||||
default=[
|
||||
'UserRoles',
|
||||
'UserDetail',
|
||||
'UserResetPassword',
|
||||
'UserList',
|
||||
'RoleList',
|
||||
"UserRoles",
|
||||
"UserDetail",
|
||||
"UserResetPassword",
|
||||
"UserList",
|
||||
"RoleList",
|
||||
],
|
||||
# NOTE(adriant): for testing purposes we include ALL default APIs
|
||||
test_default=[
|
||||
'UserRoles',
|
||||
'UserDetail',
|
||||
'UserResetPassword',
|
||||
'UserList',
|
||||
'RoleList',
|
||||
'SignUp',
|
||||
'UpdateProjectQuotas',
|
||||
'CreateProjectAndUser',
|
||||
'InviteUser',
|
||||
'ResetPassword',
|
||||
'EditUser',
|
||||
'UpdateEmail',
|
||||
"UserRoles",
|
||||
"UserDetail",
|
||||
"UserResetPassword",
|
||||
"UserList",
|
||||
"RoleList",
|
||||
"SignUp",
|
||||
"UpdateProjectQuotas",
|
||||
"CreateProjectAndUser",
|
||||
"InviteUser",
|
||||
"ResetPassword",
|
||||
"EditUser",
|
||||
"UpdateEmail",
|
||||
],
|
||||
)
|
||||
)
|
||||
|
@ -49,7 +49,7 @@ config_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"secure_proxy_ssl_header",
|
||||
help_text="The header representing a HTTP header/value combination "
|
||||
"that signifies a request is secure.",
|
||||
"that signifies a request is secure.",
|
||||
default="HTTP_X_FORWARDED_PROTO",
|
||||
)
|
||||
)
|
||||
@ -57,7 +57,7 @@ config_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"secure_proxy_ssl_header_value",
|
||||
help_text="The value representing a HTTP header/value combination "
|
||||
"that signifies a request is secure.",
|
||||
"that signifies a request is secure.",
|
||||
default="https",
|
||||
)
|
||||
)
|
||||
@ -83,7 +83,7 @@ config_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"log_file",
|
||||
help_text="The name and location of the Adjutant log file, "
|
||||
"superceded by 'adjutant.django.logging'.",
|
||||
"superceded by 'adjutant.django.logging'.",
|
||||
default="adjutant.log",
|
||||
)
|
||||
)
|
||||
|
@ -50,23 +50,14 @@ config_group.register_child_config(
|
||||
check_value_type=True,
|
||||
is_json=True,
|
||||
default={
|
||||
'admin': [
|
||||
'project_admin',
|
||||
'project_mod',
|
||||
'heat_stack_owner',
|
||||
'member',
|
||||
],
|
||||
'project_admin': [
|
||||
'project_admin',
|
||||
'project_mod',
|
||||
'heat_stack_owner',
|
||||
'member',
|
||||
],
|
||||
'project_mod': [
|
||||
'project_mod',
|
||||
'heat_stack_owner',
|
||||
'member',
|
||||
"admin": ["project_admin", "project_mod", "heat_stack_owner", "member",],
|
||||
"project_admin": [
|
||||
"project_admin",
|
||||
"project_mod",
|
||||
"heat_stack_owner",
|
||||
"member",
|
||||
],
|
||||
"project_mod": ["project_mod", "heat_stack_owner", "member",],
|
||||
},
|
||||
test_default={
|
||||
"admin": ["project_admin", "project_mod", "member", "heat_stack_owner"],
|
||||
|
@ -18,36 +18,32 @@ from confspirator import types
|
||||
|
||||
|
||||
DEFAULT_QUOTA_SIZES = {
|
||||
'small': {
|
||||
'nova': {
|
||||
'instances': 10,
|
||||
'cores': 20,
|
||||
'ram': 65536,
|
||||
'floating_ips': 10,
|
||||
'fixed_ips': 0,
|
||||
'metadata_items': 128,
|
||||
'injected_files': 5,
|
||||
'injected_file_content_bytes': 10240,
|
||||
'key_pairs': 50,
|
||||
'security_groups': 20,
|
||||
'security_group_rules': 100,
|
||||
"small": {
|
||||
"nova": {
|
||||
"instances": 10,
|
||||
"cores": 20,
|
||||
"ram": 65536,
|
||||
"floating_ips": 10,
|
||||
"fixed_ips": 0,
|
||||
"metadata_items": 128,
|
||||
"injected_files": 5,
|
||||
"injected_file_content_bytes": 10240,
|
||||
"key_pairs": 50,
|
||||
"security_groups": 20,
|
||||
"security_group_rules": 100,
|
||||
},
|
||||
'cinder': {
|
||||
'gigabytes': 5000,
|
||||
'snapshots': 50,
|
||||
'volumes': 20,
|
||||
},
|
||||
'neutron': {
|
||||
'floatingip': 10,
|
||||
'network': 3,
|
||||
'port': 50,
|
||||
'router': 3,
|
||||
'security_group': 20,
|
||||
'security_group_rule': 100,
|
||||
'subnet': 3,
|
||||
"cinder": {"gigabytes": 5000, "snapshots": 50, "volumes": 20,},
|
||||
"neutron": {
|
||||
"floatingip": 10,
|
||||
"network": 3,
|
||||
"port": 50,
|
||||
"router": 3,
|
||||
"security_group": 20,
|
||||
"security_group_rule": 100,
|
||||
"subnet": 3,
|
||||
},
|
||||
"octavia": {
|
||||
'health_monitor': 5,
|
||||
"health_monitor": 5,
|
||||
"listener": 1,
|
||||
"load_balancer": 1,
|
||||
"member": 2,
|
||||
@ -55,11 +51,7 @@ DEFAULT_QUOTA_SIZES = {
|
||||
},
|
||||
},
|
||||
"medium": {
|
||||
"cinder": {
|
||||
"gigabytes": 10000,
|
||||
"volumes": 100,
|
||||
"snapshots": 300
|
||||
},
|
||||
"cinder": {"gigabytes": 10000, "volumes": 100, "snapshots": 300},
|
||||
"nova": {
|
||||
"metadata_items": 128,
|
||||
"injected_file_content_bytes": 10240,
|
||||
@ -71,7 +63,7 @@ DEFAULT_QUOTA_SIZES = {
|
||||
"injected_files": 5,
|
||||
"cores": 100,
|
||||
"fixed_ips": 0,
|
||||
"security_groups": 50
|
||||
"security_groups": 50,
|
||||
},
|
||||
"neutron": {
|
||||
"security_group_rule": 400,
|
||||
@ -80,10 +72,10 @@ DEFAULT_QUOTA_SIZES = {
|
||||
"floatingip": 25,
|
||||
"security_group": 50,
|
||||
"router": 5,
|
||||
"port": 250
|
||||
"port": 250,
|
||||
},
|
||||
"octavia": {
|
||||
'health_monitor': 50,
|
||||
"health_monitor": 50,
|
||||
"listener": 5,
|
||||
"load_balancer": 5,
|
||||
"member": 5,
|
||||
@ -91,11 +83,7 @@ DEFAULT_QUOTA_SIZES = {
|
||||
},
|
||||
},
|
||||
"large": {
|
||||
"cinder": {
|
||||
"gigabytes": 50000,
|
||||
"volumes": 200,
|
||||
"snapshots": 600
|
||||
},
|
||||
"cinder": {"gigabytes": 50000, "volumes": 200, "snapshots": 600},
|
||||
"nova": {
|
||||
"metadata_items": 128,
|
||||
"injected_file_content_bytes": 10240,
|
||||
@ -107,7 +95,7 @@ DEFAULT_QUOTA_SIZES = {
|
||||
"injected_files": 5,
|
||||
"cores": 200,
|
||||
"fixed_ips": 0,
|
||||
"security_groups": 100
|
||||
"security_groups": 100,
|
||||
},
|
||||
"neutron": {
|
||||
"security_group_rule": 800,
|
||||
@ -116,10 +104,10 @@ DEFAULT_QUOTA_SIZES = {
|
||||
"floatingip": 50,
|
||||
"security_group": 100,
|
||||
"router": 10,
|
||||
"port": 500
|
||||
"port": 500,
|
||||
},
|
||||
"octavia": {
|
||||
'health_monitor': 100,
|
||||
"health_monitor": 100,
|
||||
"listener": 10,
|
||||
"load_balancer": 10,
|
||||
"member": 10,
|
||||
@ -145,16 +133,16 @@ config_group.register_child_config(
|
||||
fields.ListConfig(
|
||||
"sizes_ascending",
|
||||
help_text="An ascending list of all the quota size names, "
|
||||
"so that Adjutant knows their relative sizes/order.",
|
||||
default=['small', 'medium', 'large'],
|
||||
"so that Adjutant knows their relative sizes/order.",
|
||||
default=["small", "medium", "large"],
|
||||
)
|
||||
)
|
||||
config_group.register_child_config(
|
||||
fields.DictConfig(
|
||||
"services",
|
||||
help_text="A per region definition of what services Adjutant should manage "
|
||||
"quotas for. '*' means all or default region.",
|
||||
"quotas for. '*' means all or default region.",
|
||||
value_type=types.List(),
|
||||
default={'*': ['cinder', 'neutron', 'nova']},
|
||||
default={"*": ["cinder", "neutron", "nova"]},
|
||||
)
|
||||
)
|
||||
|
@ -50,31 +50,34 @@ def _build_default_email_group(
|
||||
fields.StrConfig(
|
||||
"subject",
|
||||
help_text="Default email subject for this stage",
|
||||
default=email_subject)
|
||||
default=email_subject,
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"from",
|
||||
help_text="Default from email for this stage",
|
||||
default=email_from)
|
||||
"from", help_text="Default from email for this stage", default=email_from
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"reply",
|
||||
help_text="Default reply-to email for this stage",
|
||||
default=email_reply)
|
||||
default=email_reply,
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"template",
|
||||
help_text="Default email template for this stage",
|
||||
default=email_template)
|
||||
default=email_template,
|
||||
)
|
||||
)
|
||||
email_group.register_child_config(
|
||||
fields.StrConfig(
|
||||
"html_template",
|
||||
help_text="Default email html template for this stage",
|
||||
default=email_html_template)
|
||||
default=email_html_template,
|
||||
)
|
||||
)
|
||||
return email_group
|
||||
|
||||
@ -123,9 +126,7 @@ _notifications_defaults_group.register_child_config(
|
||||
"standard_handlers",
|
||||
help_text="Handlers to use for standard notifications.",
|
||||
required=True,
|
||||
default=[
|
||||
'EmailNotification',
|
||||
],
|
||||
default=["EmailNotification",],
|
||||
)
|
||||
)
|
||||
_notifications_defaults_group.register_child_config(
|
||||
@ -133,9 +134,7 @@ _notifications_defaults_group.register_child_config(
|
||||
"error_handlers",
|
||||
help_text="Handlers to use for error notifications.",
|
||||
required=True,
|
||||
default=[
|
||||
'EmailNotification',
|
||||
],
|
||||
default=["EmailNotification",],
|
||||
)
|
||||
)
|
||||
_notifications_defaults_group.register_child_config(
|
||||
@ -159,7 +158,7 @@ _notifications_defaults_group.register_child_config(
|
||||
"safe_errors",
|
||||
help_text="Error types which are safe to acknowledge automatically.",
|
||||
required=True,
|
||||
default=['SMTPException'],
|
||||
default=["SMTPException"],
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -36,28 +36,23 @@ class AdjutantCore(BaseFeatureSet):
|
||||
project_actions.NewProjectWithUserAction,
|
||||
project_actions.NewProjectAction,
|
||||
project_actions.AddDefaultUsersToProjectAction,
|
||||
|
||||
resource_actions.NewDefaultNetworkAction,
|
||||
resource_actions.NewProjectDefaultNetworkAction,
|
||||
resource_actions.SetProjectQuotaAction,
|
||||
resource_actions.UpdateProjectQuotasAction,
|
||||
|
||||
user_actions.NewUserAction,
|
||||
user_actions.ResetUserPasswordAction,
|
||||
user_actions.EditUserRolesAction,
|
||||
user_actions.UpdateUserEmailAction,
|
||||
|
||||
misc_actions.SendAdditionalEmailAction,
|
||||
]
|
||||
|
||||
tasks = [
|
||||
project_tasks.CreateProjectAndUser,
|
||||
|
||||
user_tasks.EditUserRoles,
|
||||
user_tasks.InviteUser,
|
||||
user_tasks.ResetUserPassword,
|
||||
user_tasks.UpdateUserEmail,
|
||||
|
||||
resource_tasks.UpdateProjectQuotas,
|
||||
]
|
||||
|
||||
@ -67,7 +62,6 @@ class AdjutantCore(BaseFeatureSet):
|
||||
task_apis.ResetPassword,
|
||||
task_apis.EditUser,
|
||||
task_apis.UpdateEmail,
|
||||
|
||||
openstack_apis.UserList,
|
||||
openstack_apis.UserDetail,
|
||||
openstack_apis.UserRoles,
|
||||
|
@ -24,6 +24,7 @@ class BaseServiceException(Exception):
|
||||
If thrown during the course of an API call will be caught and returned
|
||||
to the user as an ServiceUnavailable error with a 503 response.
|
||||
"""
|
||||
|
||||
default_message = "A internal service error has occured."
|
||||
|
||||
def __init__(self, message=None):
|
||||
@ -34,28 +35,23 @@ class BaseServiceException(Exception):
|
||||
|
||||
|
||||
class InvalidActionClass(BaseServiceException):
|
||||
default_message = (
|
||||
"Cannot register action not built off the BaseAction class.")
|
||||
default_message = "Cannot register action not built off the BaseAction class."
|
||||
|
||||
|
||||
class InvalidActionSerializer(BaseServiceException):
|
||||
default_message = (
|
||||
"Action serializer must be a valid DRF serializer.")
|
||||
default_message = "Action serializer must be a valid DRF serializer."
|
||||
|
||||
|
||||
class InvalidTaskClass(BaseServiceException):
|
||||
default_message = (
|
||||
"Action serializer must be a valid DRF serializer.")
|
||||
default_message = "Action serializer must be a valid DRF serializer."
|
||||
|
||||
|
||||
class InvalidAPIClass(BaseServiceException):
|
||||
default_message = (
|
||||
"Cannot register task not built off the BaseTask class.")
|
||||
default_message = "Cannot register task not built off the BaseTask class."
|
||||
|
||||
|
||||
class DelegateAPINotRegistered(BaseServiceException):
|
||||
default_message = (
|
||||
"Failed to setup DelegateAPI that has not been registered.")
|
||||
default_message = "Failed to setup DelegateAPI that has not been registered."
|
||||
|
||||
|
||||
class TaskNotRegistered(BaseServiceException):
|
||||
@ -76,6 +72,7 @@ class ConfigurationException(BaseServiceException):
|
||||
|
||||
class BaseAPIException(Exception):
|
||||
"""An Task error occurred."""
|
||||
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def __init__(self, message=None, internal_message=None):
|
||||
@ -95,17 +92,17 @@ class BaseAPIException(Exception):
|
||||
|
||||
class NotFound(BaseAPIException):
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
default_message = 'Not found.'
|
||||
default_message = "Not found."
|
||||
|
||||
|
||||
class TaskNotFound(NotFound):
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
default_message = 'Task not found.'
|
||||
default_message = "Task not found."
|
||||
|
||||
|
||||
class ServiceUnavailable(BaseAPIException):
|
||||
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
default_message = 'Service temporarily unavailable, try again later.'
|
||||
default_message = "Service temporarily unavailable, try again later."
|
||||
|
||||
|
||||
class TaskSerializersInvalid(BaseAPIException):
|
||||
@ -145,5 +142,6 @@ class TaskStateInvalid(BaseTaskException):
|
||||
|
||||
class TaskActionsFailed(BaseTaskException):
|
||||
"""For use when Task processing fails and we want to wrap that."""
|
||||
|
||||
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
default_message = 'Service temporarily unavailable, try again later.'
|
||||
default_message = "Service temporarily unavailable, try again later."
|
||||
|
@ -43,14 +43,13 @@ from adjutant.config.feature_sets import config_group as feature_set_config
|
||||
def register_action_class(action_class):
|
||||
if not issubclass(action_class, BaseAction):
|
||||
raise exceptions.InvalidActionClass(
|
||||
"'%s' is not a built off the BaseAction class."
|
||||
% action_class.__name__
|
||||
"'%s' is not a built off the BaseAction class." % action_class.__name__
|
||||
)
|
||||
if action_class.serializer and not issubclass(
|
||||
action_class.serializer, drf_serializers.Serializer):
|
||||
action_class.serializer, drf_serializers.Serializer
|
||||
):
|
||||
raise exceptions.InvalidActionSerializer(
|
||||
"serializer for '%s' is not a valid DRF serializer."
|
||||
% action_class.__name__
|
||||
"serializer for '%s' is not a valid DRF serializer." % action_class.__name__
|
||||
)
|
||||
data = {}
|
||||
data[action_class.__name__] = action_class
|
||||
@ -59,16 +58,14 @@ def register_action_class(action_class):
|
||||
# NOTE(adriant): We copy the config_group before naming it
|
||||
# to avoid cases where a subclass inherits but doesn't extend it
|
||||
setting_group = action_class.config_group.copy()
|
||||
setting_group.set_name(
|
||||
action_class.__name__, reformat_name=False)
|
||||
setting_group.set_name(action_class.__name__, reformat_name=False)
|
||||
action_defaults_group.register_child_config(setting_group)
|
||||
|
||||
|
||||
def register_task_class(task_class):
|
||||
if not issubclass(task_class, tasks_base.BaseTask):
|
||||
raise exceptions.InvalidTaskClass(
|
||||
"'%s' is not a built off the BaseTask class."
|
||||
% task_class.__name__
|
||||
"'%s' is not a built off the BaseTask class." % task_class.__name__
|
||||
)
|
||||
data = {}
|
||||
data[task_class.task_type] = task_class
|
||||
@ -78,16 +75,14 @@ def register_task_class(task_class):
|
||||
tasks.TASK_CLASSES.update(data)
|
||||
|
||||
config_group = tasks_base.make_task_config(task_class)
|
||||
config_group.set_name(
|
||||
task_class.task_type, reformat_name=False)
|
||||
config_group.set_name(task_class.task_type, reformat_name=False)
|
||||
tasks_group.register_child_config(config_group)
|
||||
|
||||
|
||||
def register_delegate_api_class(api_class):
|
||||
if not issubclass(api_class, BaseDelegateAPI):
|
||||
raise exceptions.InvalidAPIClass(
|
||||
"'%s' is not a built off the BaseDelegateAPI class."
|
||||
% api_class.__name__
|
||||
"'%s' is not a built off the BaseDelegateAPI class." % api_class.__name__
|
||||
)
|
||||
data = {}
|
||||
data[api_class.__name__] = api_class
|
||||
@ -96,8 +91,7 @@ def register_delegate_api_class(api_class):
|
||||
# NOTE(adriant): We copy the config_group before naming it
|
||||
# to avoid cases where a subclass inherits but doesn't extend it
|
||||
setting_group = api_class.config_group.copy()
|
||||
setting_group.set_name(
|
||||
api_class.__name__, reformat_name=False)
|
||||
setting_group.set_name(api_class.__name__, reformat_name=False)
|
||||
api_config.register_child_config(setting_group)
|
||||
|
||||
|
||||
@ -121,7 +115,8 @@ def register_notification_handler(notification_handler):
|
||||
def register_feature_set_config(feature_set_group):
|
||||
if not isinstance(feature_set_group, groups.ConfigGroup):
|
||||
raise conf_exceptions.InvalidConfigClass(
|
||||
"'%s' is not a valid config group class" % feature_set_group)
|
||||
"'%s' is not a valid config group class" % feature_set_group
|
||||
)
|
||||
feature_set_config.register_child_config(feature_set_group)
|
||||
|
||||
|
||||
@ -149,7 +144,7 @@ class BaseFeatureSet(object):
|
||||
config = None
|
||||
|
||||
def __init__(self):
|
||||
self.logger = getLogger('adjutant')
|
||||
self.logger = getLogger("adjutant")
|
||||
|
||||
def load(self):
|
||||
self.logger.info("Loading feature set: '%s'" % self.__class__.__name__)
|
||||
|
@ -22,20 +22,21 @@ class KeystoneHeaderUnwrapper:
|
||||
Middleware to build an easy to use dict of important data from
|
||||
what the keystone wsgi middleware gives us.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
try:
|
||||
token_data = {
|
||||
'project_domain_id': request.META['HTTP_X_PROJECT_DOMAIN_ID'],
|
||||
'project_name': request.META['HTTP_X_PROJECT_NAME'],
|
||||
'project_id': request.META['HTTP_X_PROJECT_ID'],
|
||||
'roles': request.META['HTTP_X_ROLES'].split(','),
|
||||
'user_domain_id': request.META['HTTP_X_USER_DOMAIN_ID'],
|
||||
'username': request.META['HTTP_X_USER_NAME'],
|
||||
'user_id': request.META['HTTP_X_USER_ID'],
|
||||
'authenticated': request.META['HTTP_X_IDENTITY_STATUS']
|
||||
"project_domain_id": request.META["HTTP_X_PROJECT_DOMAIN_ID"],
|
||||
"project_name": request.META["HTTP_X_PROJECT_NAME"],
|
||||
"project_id": request.META["HTTP_X_PROJECT_ID"],
|
||||
"roles": request.META["HTTP_X_ROLES"].split(","),
|
||||
"user_domain_id": request.META["HTTP_X_USER_DOMAIN_ID"],
|
||||
"username": request.META["HTTP_X_USER_NAME"],
|
||||
"user_id": request.META["HTTP_X_USER_ID"],
|
||||
"authenticated": request.META["HTTP_X_IDENTITY_STATUS"],
|
||||
}
|
||||
except KeyError:
|
||||
token_data = {}
|
||||
@ -49,6 +50,7 @@ class TestingHeaderUnwrapper:
|
||||
"""
|
||||
Replacement for the KeystoneHeaderUnwrapper for testing purposes.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
@ -58,17 +60,18 @@ class TestingHeaderUnwrapper:
|
||||
# TODO(adriant): follow up patch to update all the test
|
||||
# headers to provide domain values.
|
||||
# Default here is just a temporary measure.
|
||||
'project_domain_id':
|
||||
request.META['headers'].get(
|
||||
'project_domain_id', 'default'),
|
||||
'project_name': request.META['headers']['project_name'],
|
||||
'project_id': request.META['headers']['project_id'],
|
||||
'roles': request.META['headers']['roles'].split(','),
|
||||
'user_domain_id':
|
||||
request.META['headers'].get('user_domain_id', 'default'),
|
||||
'username': request.META['headers']['username'],
|
||||
'user_id': request.META['headers']['user_id'],
|
||||
'authenticated': request.META['headers']['authenticated']
|
||||
"project_domain_id": request.META["headers"].get(
|
||||
"project_domain_id", "default"
|
||||
),
|
||||
"project_name": request.META["headers"]["project_name"],
|
||||
"project_id": request.META["headers"]["project_id"],
|
||||
"roles": request.META["headers"]["roles"].split(","),
|
||||
"user_domain_id": request.META["headers"].get(
|
||||
"user_domain_id", "default"
|
||||
),
|
||||
"username": request.META["headers"]["username"],
|
||||
"user_id": request.META["headers"]["user_id"],
|
||||
"authenticated": request.META["headers"]["authenticated"],
|
||||
}
|
||||
except KeyError:
|
||||
token_data = {}
|
||||
@ -86,29 +89,29 @@ class RequestLoggingMiddleware:
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
self.logger = getLogger('adjutant')
|
||||
self.logger = getLogger("adjutant")
|
||||
|
||||
def __call__(self, request):
|
||||
self.logger.info(
|
||||
'(%s) - <%s> %s [%s]',
|
||||
"(%s) - <%s> %s [%s]",
|
||||
timezone.now(),
|
||||
request.method,
|
||||
request.META['REMOTE_ADDR'],
|
||||
request.get_full_path()
|
||||
request.META["REMOTE_ADDR"],
|
||||
request.get_full_path(),
|
||||
)
|
||||
request.timer = time()
|
||||
|
||||
response = self.get_response(request)
|
||||
|
||||
if hasattr(request, 'timer'):
|
||||
if hasattr(request, "timer"):
|
||||
time_delta = time() - request.timer
|
||||
else:
|
||||
time_delta = -1
|
||||
self.logger.info(
|
||||
'(%s) - <%s> [%s] - (%.1fs)',
|
||||
"(%s) - <%s> [%s] - (%.1fs)",
|
||||
timezone.now(),
|
||||
response.status_code,
|
||||
request.get_full_path(),
|
||||
time_delta
|
||||
time_delta,
|
||||
)
|
||||
return response
|
||||
|
@ -17,11 +17,7 @@ from adjutant.api.models import Notification
|
||||
|
||||
|
||||
def create_notification(task, notes, error=False, handlers=True):
|
||||
notification = Notification.objects.create(
|
||||
task=task,
|
||||
notes=notes,
|
||||
error=error
|
||||
)
|
||||
notification = Notification.objects.create(task=task, notes=notes, error=error)
|
||||
notification.save()
|
||||
|
||||
if not handlers:
|
||||
|
@ -34,7 +34,8 @@ class BaseNotificationHandler(object):
|
||||
"""
|
||||
try:
|
||||
notif_config = CONF.notifications.handler_defaults.get(
|
||||
self.__class__.__name__)
|
||||
self.__class__.__name__
|
||||
)
|
||||
except KeyError:
|
||||
# Handler has no config
|
||||
return {}
|
||||
@ -44,10 +45,12 @@ class BaseNotificationHandler(object):
|
||||
try:
|
||||
if notification.error:
|
||||
task_defaults = task_defaults.error_handler_config.get(
|
||||
self.__class__.__name__)
|
||||
self.__class__.__name__
|
||||
)
|
||||
else:
|
||||
task_defaults = task_defaults.standard_handler_config.get(
|
||||
self.__class__.__name__)
|
||||
self.__class__.__name__
|
||||
)
|
||||
except KeyError:
|
||||
task_defaults = {}
|
||||
|
||||
|
@ -57,12 +57,11 @@ class EmailNotification(base.BaseNotificationHandler):
|
||||
fields.StrConfig(
|
||||
"template",
|
||||
help_text="Email template for this notification. "
|
||||
"No template will cause the email not to send.",
|
||||
"No template will cause the email not to send.",
|
||||
default="notification.txt",
|
||||
),
|
||||
fields.StrConfig(
|
||||
"html_template",
|
||||
help_text="Email html template for this notification.",
|
||||
"html_template", help_text="Email html template for this notification.",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
@ -22,39 +22,40 @@ from confspirator.tests import utils as conf_utils
|
||||
|
||||
from adjutant.api.models import Notification
|
||||
from adjutant.tasks.models import Task
|
||||
from adjutant.common.tests.fake_clients import (
|
||||
FakeManager, setup_identity_cache)
|
||||
from adjutant.common.tests.fake_clients import FakeManager, setup_identity_cache
|
||||
from adjutant.common.tests.utils import AdjutantAPITestCase
|
||||
from adjutant.config import CONF
|
||||
from adjutant import exceptions
|
||||
|
||||
|
||||
@mock.patch('adjutant.common.user_store.IdentityManager',
|
||||
FakeManager)
|
||||
@mock.patch("adjutant.common.user_store.IdentityManager", FakeManager)
|
||||
@conf_utils.modify_conf(
|
||||
CONF,
|
||||
operations={
|
||||
"adjutant.workflow.tasks.create_project_and_user.notifications": [
|
||||
{'operation': 'override', 'value': {
|
||||
"standard_handlers": ["EmailNotification"],
|
||||
"error_handlers": ["EmailNotification"],
|
||||
"standard_handler_config": {
|
||||
"EmailNotification": {
|
||||
'emails': ['example_notification@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
}
|
||||
{
|
||||
"operation": "override",
|
||||
"value": {
|
||||
"standard_handlers": ["EmailNotification"],
|
||||
"error_handlers": ["EmailNotification"],
|
||||
"standard_handler_config": {
|
||||
"EmailNotification": {
|
||||
"emails": ["example_notification@example.com"],
|
||||
"reply": "no-reply@example.com",
|
||||
}
|
||||
},
|
||||
"error_handler_config": {
|
||||
"EmailNotification": {
|
||||
"emails": ["example_error_notification@example.com"],
|
||||
"reply": "no-reply@example.com",
|
||||
}
|
||||
},
|
||||
},
|
||||
"error_handler_config": {
|
||||
"EmailNotification": {
|
||||
'emails': ['example_error_notification@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
}
|
||||
},
|
||||
}},
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
)
|
||||
class NotificationTests(AdjutantAPITestCase):
|
||||
|
||||
def test_new_project_sends_notification(self):
|
||||
"""
|
||||
Confirm that the email notification handler correctly acknowledges
|
||||
@ -65,15 +66,15 @@ class NotificationTests(AdjutantAPITestCase):
|
||||
setup_identity_cache()
|
||||
|
||||
url = "/v1/openstack/sign-up"
|
||||
data = {'project_name': "test_project", 'email': "test@example.com"}
|
||||
response = self.client.post(url, data, format='json')
|
||||
data = {"project_name": "test_project", "email": "test@example.com"}
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
|
||||
new_task = Task.objects.all()[0]
|
||||
self.assertEqual(Notification.objects.count(), 1)
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
self.assertEqual(mail.outbox[1].subject, "create_project_and_user notification")
|
||||
self.assertEqual(mail.outbox[1].to, ['example_notification@example.com'])
|
||||
self.assertEqual(mail.outbox[1].to, ["example_notification@example.com"])
|
||||
|
||||
notif = Notification.objects.all()[0]
|
||||
self.assertEqual(notif.task.uuid, new_task.uuid)
|
||||
@ -81,12 +82,12 @@ class NotificationTests(AdjutantAPITestCase):
|
||||
self.assertTrue(notif.acknowledged)
|
||||
|
||||
headers = {
|
||||
'project_name': "test_project",
|
||||
'project_id': "test_project_id",
|
||||
'roles': "admin,member",
|
||||
'username': "test@example.com",
|
||||
'user_id': "test_user_id",
|
||||
'authenticated': True
|
||||
"project_name": "test_project",
|
||||
"project_id": "test_project_id",
|
||||
"roles": "admin,member",
|
||||
"username": "test@example.com",
|
||||
"user_id": "test_user_id",
|
||||
"authenticated": True,
|
||||
}
|
||||
url = "/v1/tasks/" + new_task.uuid
|
||||
with mock.patch(
|
||||
@ -102,8 +103,10 @@ class NotificationTests(AdjutantAPITestCase):
|
||||
# should send token email, but no new notification
|
||||
self.assertEqual(Notification.objects.count(), 2)
|
||||
self.assertEqual(len(mail.outbox), 3)
|
||||
self.assertEqual(mail.outbox[2].subject, "Error - create_project_and_user notification")
|
||||
self.assertEqual(mail.outbox[2].to, ['example_error_notification@example.com'])
|
||||
self.assertEqual(
|
||||
mail.outbox[2].subject, "Error - create_project_and_user notification"
|
||||
)
|
||||
self.assertEqual(mail.outbox[2].to, ["example_error_notification@example.com"])
|
||||
|
||||
notif = Notification.objects.all()[1]
|
||||
self.assertEqual(notif.task.uuid, new_task.uuid)
|
||||
|
@ -33,41 +33,40 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework_swagger',
|
||||
|
||||
'adjutant.commands',
|
||||
'adjutant.actions',
|
||||
'adjutant.api',
|
||||
'adjutant.notifications',
|
||||
'adjutant.tasks',
|
||||
'adjutant.startup',
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"rest_framework",
|
||||
"rest_framework_swagger",
|
||||
"adjutant.commands",
|
||||
"adjutant.actions",
|
||||
"adjutant.api",
|
||||
"adjutant.notifications",
|
||||
"adjutant.tasks",
|
||||
"adjutant.startup",
|
||||
)
|
||||
|
||||
MIDDLEWARE = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'adjutant.middleware.KeystoneHeaderUnwrapper',
|
||||
'adjutant.middleware.RequestLoggingMiddleware'
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"adjutant.middleware.KeystoneHeaderUnwrapper",
|
||||
"adjutant.middleware.RequestLoggingMiddleware",
|
||||
)
|
||||
|
||||
if 'test' in sys.argv:
|
||||
if "test" in sys.argv:
|
||||
# modify MIDDLEWARE
|
||||
MIDDLEWARE = list(MIDDLEWARE)
|
||||
MIDDLEWARE.remove('adjutant.middleware.KeystoneHeaderUnwrapper')
|
||||
MIDDLEWARE.append('adjutant.middleware.TestingHeaderUnwrapper')
|
||||
MIDDLEWARE.remove("adjutant.middleware.KeystoneHeaderUnwrapper")
|
||||
MIDDLEWARE.append("adjutant.middleware.TestingHeaderUnwrapper")
|
||||
|
||||
ROOT_URLCONF = 'adjutant.urls'
|
||||
ROOT_URLCONF = "adjutant.urls"
|
||||
|
||||
WSGI_APPLICATION = 'adjutant.wsgi.application'
|
||||
WSGI_APPLICATION = "adjutant.wsgi.application"
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@ -75,33 +74,29 @@ USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'APP_DIRS': True,
|
||||
'NAME': 'default',
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"APP_DIRS": True,
|
||||
"NAME": "default",
|
||||
},
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'APP_DIRS': True,
|
||||
'DIRS': ['/etc/adjutant/templates/'],
|
||||
'NAME': 'include_etc_templates',
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"APP_DIRS": True,
|
||||
"DIRS": ["/etc/adjutant/templates/"],
|
||||
"NAME": "include_etc_templates",
|
||||
},
|
||||
]
|
||||
|
||||
AUTHENTICATION_BACKENDS = []
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'EXCEPTION_HANDLER': 'adjutant.api.exception_handler.exception_handler',
|
||||
'DEFAULT_RENDERER_CLASSES': [
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
],
|
||||
'DEFAULT_PARSER_CLASSES': [
|
||||
'rest_framework.parsers.JSONParser',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [],
|
||||
"EXCEPTION_HANDLER": "adjutant.api.exception_handler.exception_handler",
|
||||
"DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer",],
|
||||
"DEFAULT_PARSER_CLASSES": ["rest_framework.parsers.JSONParser",],
|
||||
"DEFAULT_PERMISSION_CLASSES": [],
|
||||
}
|
||||
|
||||
SECRET_KEY = adj_conf.django.secret_key
|
||||
@ -109,14 +104,15 @@ SECRET_KEY = adj_conf.django.secret_key
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = adj_conf.django.debug
|
||||
if DEBUG:
|
||||
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append(
|
||||
'rest_framework.renderers.BrowsableAPIRenderer')
|
||||
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append(
|
||||
"rest_framework.renderers.BrowsableAPIRenderer"
|
||||
)
|
||||
|
||||
ALLOWED_HOSTS = adj_conf.django.allowed_hosts
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = (
|
||||
adj_conf.django.secure_proxy_ssl_header,
|
||||
adj_conf.django.secure_proxy_ssl_header_value
|
||||
adj_conf.django.secure_proxy_ssl_header_value,
|
||||
)
|
||||
|
||||
DATABASES = adj_conf.django.databases
|
||||
@ -125,30 +121,22 @@ if adj_conf.django.logging:
|
||||
LOGGING = adj_conf.django.logging
|
||||
else:
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'file': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': adj_conf.django.log_file,
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"file": {
|
||||
"level": "INFO",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": adj_conf.django.log_file,
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'adjutant': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'django': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'keystonemiddleware': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
"loggers": {
|
||||
"adjutant": {"handlers": ["file"], "level": "INFO", "propagate": False,},
|
||||
"django": {"handlers": ["file"], "level": "INFO", "propagate": False,},
|
||||
"keystonemiddleware": {
|
||||
"handlers": ["file"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
default_app_config = 'adjutant.startup.config.StartUpConfig'
|
||||
default_app_config = "adjutant.startup.config.StartUpConfig"
|
||||
|
@ -19,14 +19,15 @@ from adjutant.exceptions import ActionNotRegistered, DelegateAPINotRegistered
|
||||
|
||||
def check_expected_delegate_apis():
|
||||
missing_delegate_apis = list(
|
||||
set(CONF.api.active_delegate_apis)
|
||||
- set(api.DELEGATE_API_CLASSES.keys()))
|
||||
set(CONF.api.active_delegate_apis) - set(api.DELEGATE_API_CLASSES.keys())
|
||||
)
|
||||
|
||||
if missing_delegate_apis:
|
||||
raise DelegateAPINotRegistered(
|
||||
message=(
|
||||
"Expected DelegateAPIs are unregistered: %s"
|
||||
% missing_delegate_apis))
|
||||
"Expected DelegateAPIs are unregistered: %s" % missing_delegate_apis
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def check_configured_actions():
|
||||
@ -38,11 +39,12 @@ def check_configured_actions():
|
||||
|
||||
configured_actions += task_class.default_actions
|
||||
configured_actions += CONF.workflow.tasks.get(
|
||||
task_class.task_type).additional_actions
|
||||
task_class.task_type
|
||||
).additional_actions
|
||||
|
||||
missing_actions = list(
|
||||
set(configured_actions) - set(actions.ACTION_CLASSES.keys()))
|
||||
missing_actions = list(set(configured_actions) - set(actions.ACTION_CLASSES.keys()))
|
||||
|
||||
if missing_actions:
|
||||
raise ActionNotRegistered(
|
||||
"Configured actions are unregistered: %s" % missing_actions)
|
||||
"Configured actions are unregistered: %s" % missing_actions
|
||||
)
|
||||
|
@ -16,6 +16,6 @@ import pkg_resources
|
||||
|
||||
|
||||
def load_feature_sets():
|
||||
for entry_point in pkg_resources.iter_entry_points('adjutant.feature_sets'):
|
||||
for entry_point in pkg_resources.iter_entry_points("adjutant.feature_sets"):
|
||||
feature_set = entry_point.load()
|
||||
feature_set().load()
|
||||
|
@ -11,33 +11,51 @@ import jsonfield.fields
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0005_auto_20190610_0209'),
|
||||
("api", "0005_auto_20190610_0209"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.CreateModel(
|
||||
name='Task',
|
||||
name="Task",
|
||||
fields=[
|
||||
('uuid', models.CharField(default=adjutant.tasks.models.hex_uuid, max_length=32, primary_key=True, serialize=False)),
|
||||
('hash_key', models.CharField(db_index=True, max_length=64)),
|
||||
('ip_address', models.GenericIPAddressField()),
|
||||
('keystone_user', jsonfield.fields.JSONField(default={})),
|
||||
('project_id', models.CharField(db_index=True, max_length=64, null=True)),
|
||||
('approved_by', jsonfield.fields.JSONField(default={})),
|
||||
('task_type', models.CharField(db_index=True, max_length=100)),
|
||||
('action_notes', jsonfield.fields.JSONField(default={})),
|
||||
('cancelled', models.BooleanField(db_index=True, default=False)),
|
||||
('approved', models.BooleanField(db_index=True, default=False)),
|
||||
('completed', models.BooleanField(db_index=True, default=False)),
|
||||
('created_on', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('approved_on', models.DateTimeField(null=True)),
|
||||
('completed_on', models.DateTimeField(null=True)),
|
||||
(
|
||||
"uuid",
|
||||
models.CharField(
|
||||
default=adjutant.tasks.models.hex_uuid,
|
||||
max_length=32,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("hash_key", models.CharField(db_index=True, max_length=64)),
|
||||
("ip_address", models.GenericIPAddressField()),
|
||||
("keystone_user", jsonfield.fields.JSONField(default={})),
|
||||
(
|
||||
"project_id",
|
||||
models.CharField(db_index=True, max_length=64, null=True),
|
||||
),
|
||||
("approved_by", jsonfield.fields.JSONField(default={})),
|
||||
("task_type", models.CharField(db_index=True, max_length=100)),
|
||||
("action_notes", jsonfield.fields.JSONField(default={})),
|
||||
(
|
||||
"cancelled",
|
||||
models.BooleanField(db_index=True, default=False),
|
||||
),
|
||||
("approved", models.BooleanField(db_index=True, default=False)),
|
||||
(
|
||||
"completed",
|
||||
models.BooleanField(db_index=True, default=False),
|
||||
),
|
||||
(
|
||||
"created_on",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("approved_on", models.DateTimeField(null=True)),
|
||||
("completed_on", models.DateTimeField(null=True)),
|
||||
],
|
||||
options={
|
||||
'indexes': [],
|
||||
},
|
||||
options={"indexes": [],},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -9,71 +9,77 @@ import jsonfield.fields
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tasks', '0001_initial'),
|
||||
("tasks", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='task',
|
||||
name='ip_address',
|
||||
),
|
||||
migrations.RemoveField(model_name="task", name="ip_address",),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='task_notes',
|
||||
model_name="task",
|
||||
name="task_notes",
|
||||
field=jsonfield.fields.JSONField(default=[]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='approved',
|
||||
model_name="task",
|
||||
name="approved",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='cancelled',
|
||||
model_name="task",
|
||||
name="cancelled",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='completed',
|
||||
model_name="task",
|
||||
name="completed",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='hash_key',
|
||||
field=models.CharField(max_length=64),
|
||||
model_name="task", name="hash_key", field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='project_id',
|
||||
model_name="task",
|
||||
name="project_id",
|
||||
field=models.CharField(max_length=64, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='task_type',
|
||||
field=models.CharField(max_length=100),
|
||||
model_name="task", name="task_type", field=models.CharField(max_length=100),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='task',
|
||||
index=models.Index(fields=['completed'], name='completed_idx'),
|
||||
model_name="task",
|
||||
index=models.Index(fields=["completed"], name="completed_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='task',
|
||||
index=models.Index(fields=['project_id', 'uuid'], name='tasks_task_project_a1cfa7_idx'),
|
||||
model_name="task",
|
||||
index=models.Index(
|
||||
fields=["project_id", "uuid"], name="tasks_task_project_a1cfa7_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='task',
|
||||
index=models.Index(fields=['project_id', 'task_type'], name='tasks_task_project_e86456_idx'),
|
||||
model_name="task",
|
||||
index=models.Index(
|
||||
fields=["project_id", "task_type"], name="tasks_task_project_e86456_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='task',
|
||||
index=models.Index(fields=['project_id', 'task_type', 'cancelled'], name='tasks_task_project_f0ec0e_idx'),
|
||||
model_name="task",
|
||||
index=models.Index(
|
||||
fields=["project_id", "task_type", "cancelled"],
|
||||
name="tasks_task_project_f0ec0e_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='task',
|
||||
index=models.Index(fields=['project_id', 'task_type', 'completed', 'cancelled'], name='tasks_task_project_1cb2a8_idx'),
|
||||
model_name="task",
|
||||
index=models.Index(
|
||||
fields=["project_id", "task_type", "completed", "cancelled"],
|
||||
name="tasks_task_project_1cb2a8_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='task',
|
||||
index=models.Index(fields=['hash_key', 'completed', 'cancelled'], name='tasks_task_hash_ke_781b6a_idx'),
|
||||
model_name="task",
|
||||
index=models.Index(
|
||||
fields=["hash_key", "completed", "cancelled"],
|
||||
name="tasks_task_hash_ke_781b6a_idx",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -31,8 +31,8 @@ class Task(models.Model):
|
||||
Stores the state of the Task and a log for the
|
||||
action.
|
||||
"""
|
||||
uuid = models.CharField(max_length=32, default=hex_uuid,
|
||||
primary_key=True)
|
||||
|
||||
uuid = models.CharField(max_length=32, default=hex_uuid, primary_key=True)
|
||||
hash_key = models.CharField(max_length=64)
|
||||
|
||||
# who is this:
|
||||
@ -61,13 +61,12 @@ class Task(models.Model):
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['completed'], name='completed_idx'),
|
||||
models.Index(fields=['project_id', 'uuid']),
|
||||
models.Index(fields=['project_id', 'task_type']),
|
||||
models.Index(fields=['project_id', 'task_type', 'cancelled']),
|
||||
models.Index(fields=[
|
||||
'project_id', 'task_type', 'completed', 'cancelled']),
|
||||
models.Index(fields=['hash_key', 'completed', 'cancelled']),
|
||||
models.Index(fields=["completed"], name="completed_idx"),
|
||||
models.Index(fields=["project_id", "uuid"]),
|
||||
models.Index(fields=["project_id", "task_type"]),
|
||||
models.Index(fields=["project_id", "task_type", "cancelled"]),
|
||||
models.Index(fields=["project_id", "task_type", "completed", "cancelled"]),
|
||||
models.Index(fields=["hash_key", "completed", "cancelled"]),
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -89,7 +88,7 @@ class Task(models.Model):
|
||||
|
||||
@property
|
||||
def actions(self):
|
||||
return self.action_set.order_by('order')
|
||||
return self.action_set.order_by("order")
|
||||
|
||||
@property
|
||||
def tokens(self):
|
||||
@ -102,11 +101,13 @@ class Task(models.Model):
|
||||
def to_dict(self):
|
||||
actions = []
|
||||
for action in self.actions:
|
||||
actions.append({
|
||||
"action_name": action.action_name,
|
||||
"data": action.action_data,
|
||||
"valid": action.valid
|
||||
})
|
||||
actions.append(
|
||||
{
|
||||
"action_name": action.action_name,
|
||||
"data": action.action_data,
|
||||
"valid": action.valid,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"uuid": self.uuid,
|
||||
|
@ -23,8 +23,7 @@ from adjutant.api.models import Task
|
||||
from adjutant.config import CONF
|
||||
from django.utils import timezone
|
||||
from adjutant.notifications.utils import create_notification
|
||||
from adjutant.tasks.v1.utils import (
|
||||
send_stage_email, create_token, handle_task_error)
|
||||
from adjutant.tasks.v1.utils import send_stage_email, create_token, handle_task_error
|
||||
from adjutant import exceptions
|
||||
|
||||
|
||||
@ -35,7 +34,7 @@ def make_task_config(task_class):
|
||||
fields.BoolConfig(
|
||||
"allow_auto_approve",
|
||||
help_text="Override if this task allows auto_approval. "
|
||||
"Otherwise uses task default.",
|
||||
"Otherwise uses task default.",
|
||||
default=task_class.allow_auto_approve,
|
||||
)
|
||||
)
|
||||
@ -43,7 +42,7 @@ def make_task_config(task_class):
|
||||
fields.ListConfig(
|
||||
"additional_actions",
|
||||
help_text="Additional actions to be run as part of the task "
|
||||
"after default actions.",
|
||||
"after default actions.",
|
||||
default=task_class.additional_actions or [],
|
||||
)
|
||||
)
|
||||
@ -51,7 +50,7 @@ def make_task_config(task_class):
|
||||
fields.IntConfig(
|
||||
"token_expiry",
|
||||
help_text="Override for the task token expiry. "
|
||||
"Otherwise uses task default.",
|
||||
"Otherwise uses task default.",
|
||||
default=task_class.token_expiry,
|
||||
)
|
||||
)
|
||||
@ -59,13 +58,11 @@ def make_task_config(task_class):
|
||||
fields.DictConfig(
|
||||
"actions",
|
||||
help_text="Action config overrides over the action defaults. "
|
||||
"See 'adjutant.workflow.action_defaults'.",
|
||||
"See 'adjutant.workflow.action_defaults'.",
|
||||
is_json=True,
|
||||
default=task_class.action_config or {},
|
||||
sample_default={
|
||||
"SomeCustomAction": {
|
||||
"some_action_setting": "<a-uuid-probably>"
|
||||
}
|
||||
"SomeCustomAction": {"some_action_setting": "<a-uuid-probably>"}
|
||||
},
|
||||
)
|
||||
)
|
||||
@ -73,14 +70,12 @@ def make_task_config(task_class):
|
||||
fields.DictConfig(
|
||||
"emails",
|
||||
help_text="Email config overrides for this task over task defaults."
|
||||
"See 'adjutant.workflow.emails'.",
|
||||
"See 'adjutant.workflow.emails'.",
|
||||
is_json=True,
|
||||
default=task_class.email_config or {},
|
||||
sample_default={
|
||||
"initial": None,
|
||||
"token": {
|
||||
"subject": "Some custom subject",
|
||||
},
|
||||
"token": {"subject": "Some custom subject",},
|
||||
},
|
||||
)
|
||||
)
|
||||
@ -88,7 +83,7 @@ def make_task_config(task_class):
|
||||
fields.DictConfig(
|
||||
"notifications",
|
||||
help_text="Notification config overrides for this task over task defaults."
|
||||
"See 'adjutant.workflow.notifications'.",
|
||||
"See 'adjutant.workflow.notifications'.",
|
||||
is_json=True,
|
||||
default=task_class.notification_config or {},
|
||||
sample_default={
|
||||
@ -96,14 +91,14 @@ def make_task_config(task_class):
|
||||
"error_handlers": ["EmailNotification"],
|
||||
"standard_handler_config": {
|
||||
"EmailNotification": {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
"emails": ["example@example.com"],
|
||||
"reply": "no-reply@example.com",
|
||||
}
|
||||
},
|
||||
"error_handler_config": {
|
||||
"EmailNotification": {
|
||||
'emails': ['example@example.com'],
|
||||
'reply': 'no-reply@example.com',
|
||||
"emails": ["example@example.com"],
|
||||
"reply": "no-reply@example.com",
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -141,50 +136,45 @@ class BaseTask(object):
|
||||
email_config = None
|
||||
notification_config = None
|
||||
|
||||
def __init__(self,
|
||||
task_model=None,
|
||||
task_data=None,
|
||||
action_data=None):
|
||||
def __init__(self, task_model=None, task_data=None, action_data=None):
|
||||
self._config = None
|
||||
self.logger = getLogger('adjutant')
|
||||
self.logger = getLogger("adjutant")
|
||||
|
||||
if task_model:
|
||||
self.task = task_model
|
||||
self._refresh_actions()
|
||||
else:
|
||||
# raises 400 validation error
|
||||
action_serializer_list = self._instantiate_action_serializers(
|
||||
action_data)
|
||||
action_serializer_list = self._instantiate_action_serializers(action_data)
|
||||
|
||||
hash_key = self._create_task_hash(action_serializer_list)
|
||||
# raises duplicate error
|
||||
self._handle_duplicates(hash_key)
|
||||
|
||||
keystone_user = task_data.get('keystone_user', {})
|
||||
keystone_user = task_data.get("keystone_user", {})
|
||||
self.task = Task.objects.create(
|
||||
keystone_user=keystone_user,
|
||||
project_id=keystone_user.get('project_id'),
|
||||
project_id=keystone_user.get("project_id"),
|
||||
task_type=self.task_type,
|
||||
hash_key=hash_key)
|
||||
hash_key=hash_key,
|
||||
)
|
||||
self.task.save()
|
||||
|
||||
# Instantiate actions with serializers
|
||||
self.actions = []
|
||||
for i, action in enumerate(action_serializer_list):
|
||||
data = action['serializer'].validated_data
|
||||
data = action["serializer"].validated_data
|
||||
|
||||
# construct the action class
|
||||
self.actions.append(action['action'](
|
||||
data=data,
|
||||
task=self.task,
|
||||
order=i
|
||||
))
|
||||
self.actions.append(
|
||||
action["action"](data=data, task=self.task, order=i)
|
||||
)
|
||||
self.logger.info(
|
||||
"(%s) - '%s' task created (%s)."
|
||||
% (timezone.now(), self.task_type, self.task.uuid))
|
||||
% (timezone.now(), self.task_type, self.task.uuid)
|
||||
)
|
||||
|
||||
def _instantiate_action_serializers(self, action_data,
|
||||
use_existing_actions=False):
|
||||
def _instantiate_action_serializers(self, action_data, use_existing_actions=False):
|
||||
action_serializer_list = []
|
||||
|
||||
if use_existing_actions:
|
||||
@ -209,13 +199,13 @@ class BaseTask(object):
|
||||
# instantiate serializer class
|
||||
if not action_class.serializer:
|
||||
raise exceptions.SerializerMissingException(
|
||||
"No serializer defined for action %s" % action_name)
|
||||
"No serializer defined for action %s" % action_name
|
||||
)
|
||||
serializer = action_class.serializer(data=action_data)
|
||||
|
||||
action_serializer_list.append({
|
||||
'name': action_name,
|
||||
'action': action_class,
|
||||
'serializer': serializer})
|
||||
action_serializer_list.append(
|
||||
{"name": action_name, "action": action_class, "serializer": serializer}
|
||||
)
|
||||
|
||||
if serializer and not serializer.is_valid():
|
||||
valid = False
|
||||
@ -223,52 +213,50 @@ class BaseTask(object):
|
||||
if not valid:
|
||||
errors = {}
|
||||
for action in action_serializer_list:
|
||||
if action['serializer']:
|
||||
errors.update(action['serializer'].errors)
|
||||
if action["serializer"]:
|
||||
errors.update(action["serializer"].errors)
|
||||
raise exceptions.TaskSerializersInvalid(errors)
|
||||
|
||||
return action_serializer_list
|
||||
|
||||
def _create_task_hash(self, action_list):
|
||||
hashable_list = [self.task_type, ]
|
||||
hashable_list = [
|
||||
self.task_type,
|
||||
]
|
||||
|
||||
for action in action_list:
|
||||
hashable_list.append(action['name'])
|
||||
if not action['serializer']:
|
||||
hashable_list.append(action["name"])
|
||||
if not action["serializer"]:
|
||||
continue
|
||||
# iterate like this to maintain consistent order for hash
|
||||
fields = sorted(action['serializer'].validated_data.keys())
|
||||
fields = sorted(action["serializer"].validated_data.keys())
|
||||
for field in fields:
|
||||
try:
|
||||
hashable_list.append(
|
||||
action['serializer'].validated_data[field])
|
||||
hashable_list.append(action["serializer"].validated_data[field])
|
||||
except KeyError:
|
||||
if field == "username" and CONF.identity.username_is_email:
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
|
||||
return hashlib.sha256(str(hashable_list).encode('utf-8')).hexdigest()
|
||||
return hashlib.sha256(str(hashable_list).encode("utf-8")).hexdigest()
|
||||
|
||||
def _handle_duplicates(self, hash_key):
|
||||
|
||||
duplicate_tasks = Task.objects.filter(
|
||||
hash_key=hash_key,
|
||||
completed=0,
|
||||
cancelled=0)
|
||||
hash_key=hash_key, completed=0, cancelled=0
|
||||
)
|
||||
|
||||
if not duplicate_tasks:
|
||||
return
|
||||
|
||||
if self.duplicate_policy == "cancel":
|
||||
now = timezone.now()
|
||||
self.logger.info(
|
||||
"(%s) - Task is a duplicate - Cancelling old tasks." %
|
||||
now)
|
||||
self.logger.info("(%s) - Task is a duplicate - Cancelling old tasks." % now)
|
||||
for task in duplicate_tasks:
|
||||
task.add_task_note(
|
||||
"Task cancelled because was an old duplicate. - (%s)"
|
||||
% now)
|
||||
"Task cancelled because was an old duplicate. - (%s)" % now
|
||||
)
|
||||
task.get_task().cancel()
|
||||
return
|
||||
|
||||
@ -288,7 +276,7 @@ class BaseTask(object):
|
||||
email_conf = self.config.emails.token
|
||||
send_stage_email(self.task, email_conf, token)
|
||||
except KeyError as e:
|
||||
handle_task_error(e, self.task, error_text='while sending token')
|
||||
handle_task_error(e, self.task, error_text="while sending token")
|
||||
|
||||
def add_note(self, note):
|
||||
"""
|
||||
@ -296,7 +284,8 @@ class BaseTask(object):
|
||||
"""
|
||||
now = timezone.now()
|
||||
self.logger.info(
|
||||
"(%s)(%s)(%s) - %s" % (now, self.task_type, self.task.uuid, note))
|
||||
"(%s)(%s)(%s) - %s" % (now, self.task_type, self.task.uuid, note)
|
||||
)
|
||||
note = "%s - (%s)" % (note, now)
|
||||
self.task.add_task_note(note)
|
||||
|
||||
@ -320,7 +309,8 @@ class BaseTask(object):
|
||||
if not valid:
|
||||
# TODO(amelia): get action invalidation reasons and raise those
|
||||
raise exceptions.TaskActionsInvalid(
|
||||
self.task, 'actions invalid', internal_message)
|
||||
self.task, "actions invalid", internal_message
|
||||
)
|
||||
|
||||
@property
|
||||
def approved(self):
|
||||
@ -342,40 +332,47 @@ class BaseTask(object):
|
||||
if completed is not None:
|
||||
if self.task.completed and not completed:
|
||||
raise exceptions.TaskStateInvalid(
|
||||
self.task, "This task has already been completed.")
|
||||
self.task, "This task has already been completed."
|
||||
)
|
||||
if not self.task.completed and completed:
|
||||
raise exceptions.TaskStateInvalid(
|
||||
self.task, "This task hasn't been completed.")
|
||||
self.task, "This task hasn't been completed."
|
||||
)
|
||||
|
||||
if cancelled is not None:
|
||||
if self.task.cancelled and not cancelled:
|
||||
raise exceptions.TaskStateInvalid(
|
||||
self.task, "This task has been cancelled.")
|
||||
self.task, "This task has been cancelled."
|
||||
)
|
||||
if not self.task.cancelled and cancelled:
|
||||
raise exceptions.TaskStateInvalid(
|
||||
self.task, "This task has not been cancelled.")
|
||||
self.task, "This task has not been cancelled."
|
||||
)
|
||||
if approved is not None:
|
||||
if self.task.approved and not approved:
|
||||
raise exceptions.TaskStateInvalid(
|
||||
self.task, "This task has already been approved.")
|
||||
self.task, "This task has already been approved."
|
||||
)
|
||||
if not self.task.approved and approved:
|
||||
raise exceptions.TaskStateInvalid(
|
||||
self.task, "This task has not been approved.")
|
||||
self.task, "This task has not been approved."
|
||||
)
|
||||
|
||||
def update(self, action_data):
|
||||
self.confirm_state(approved=False, completed=False, cancelled=False)
|
||||
|
||||
action_serializer_list = self._instantiate_action_serializers(
|
||||
action_data, use_existing_actions=True)
|
||||
action_data, use_existing_actions=True
|
||||
)
|
||||
|
||||
hash_key = self._create_task_hash(action_serializer_list)
|
||||
self._handle_duplicates(hash_key)
|
||||
|
||||
for action in action_serializer_list:
|
||||
data = action['serializer'].validated_data
|
||||
data = action["serializer"].validated_data
|
||||
|
||||
action['action'].action.action_data = data
|
||||
action['action'].action.save()
|
||||
action["action"].action.action_data = data
|
||||
action["action"].action.save()
|
||||
self._refresh_actions()
|
||||
self.prepare()
|
||||
|
||||
@ -392,8 +389,7 @@ class BaseTask(object):
|
||||
try:
|
||||
action.prepare()
|
||||
except Exception as e:
|
||||
handle_task_error(
|
||||
e, self.task, error_text='while setting up task')
|
||||
handle_task_error(e, self.task, error_text="while setting up task")
|
||||
|
||||
# send initial confirmation email:
|
||||
email_conf = self.config.emails.initial
|
||||
@ -424,10 +420,7 @@ class BaseTask(object):
|
||||
return
|
||||
|
||||
if self.send_approval_notification:
|
||||
notes = {
|
||||
'notes':
|
||||
["'%s' task needs approval." % self.task_type]
|
||||
}
|
||||
notes = {"notes": ["'%s' task needs approval." % self.task_type]}
|
||||
create_notification(self.task, notes)
|
||||
|
||||
def approve(self, approved_by="system"):
|
||||
@ -451,8 +444,7 @@ class BaseTask(object):
|
||||
try:
|
||||
action.approve()
|
||||
except Exception as e:
|
||||
handle_task_error(
|
||||
e, self.task, error_text='while approving task')
|
||||
handle_task_error(e, self.task, error_text="while approving task")
|
||||
|
||||
self.is_valid("task invalid after approval")
|
||||
|
||||
@ -495,10 +487,11 @@ class BaseTask(object):
|
||||
try:
|
||||
data[field] = token_data[field]
|
||||
except KeyError:
|
||||
errors[field] = ["This field is required.", ]
|
||||
errors[field] = [
|
||||
"This field is required.",
|
||||
]
|
||||
except TypeError:
|
||||
errors = ["Improperly formated json. "
|
||||
"Should be a key-value object."]
|
||||
errors = ["Improperly formated json. " "Should be a key-value object."]
|
||||
break
|
||||
|
||||
if errors:
|
||||
@ -510,8 +503,7 @@ class BaseTask(object):
|
||||
try:
|
||||
action.submit(data)
|
||||
except Exception as e:
|
||||
handle_task_error(
|
||||
e, self.task, "while submiting task")
|
||||
handle_task_error(e, self.task, "while submiting task")
|
||||
|
||||
self.is_valid("task invalid after submit")
|
||||
|
||||
|
@ -23,9 +23,8 @@ from adjutant.tasks.v1.base import BaseTask
|
||||
|
||||
|
||||
class TaskManager(object):
|
||||
|
||||
def __init__(self, message=None):
|
||||
self.logger = getLogger('adjutant')
|
||||
self.logger = getLogger("adjutant")
|
||||
|
||||
def _get_task_class(self, task_type):
|
||||
"""Get the task class from the given task_type
|
||||
@ -38,8 +37,7 @@ class TaskManager(object):
|
||||
except KeyError:
|
||||
if task_type in tasks.TASK_CLASSES.values():
|
||||
return task_type
|
||||
raise exceptions.TaskNotRegistered(
|
||||
"Unknown task type: '%s'" % task_type)
|
||||
raise exceptions.TaskNotRegistered("Unknown task type: '%s'" % task_type)
|
||||
|
||||
def create_from_request(self, task_type, request):
|
||||
task_class = self._get_task_class(task_type)
|
||||
@ -65,7 +63,8 @@ class TaskManager(object):
|
||||
task = Task.objects.get(uuid=task)
|
||||
except Task.DoesNotExist:
|
||||
raise exceptions.TaskNotFound(
|
||||
"Task not found with uuid of: '%s'" % task)
|
||||
"Task not found with uuid of: '%s'" % task
|
||||
)
|
||||
if isinstance(task, Task):
|
||||
try:
|
||||
return tasks.TASK_CLASSES[task.task_type](task)
|
||||
@ -74,11 +73,9 @@ class TaskManager(object):
|
||||
# for older deprecated tasks:
|
||||
raise exceptions.TaskNotRegistered(
|
||||
"Task type '%s' not registered, "
|
||||
"and used for existing task."
|
||||
% task.task_type
|
||||
"and used for existing task." % task.task_type
|
||||
)
|
||||
raise exceptions.TaskNotFound(
|
||||
"Task not found for value of: '%s'" % task)
|
||||
raise exceptions.TaskNotFound("Task not found for value of: '%s'" % task)
|
||||
|
||||
def update(self, task, action_data):
|
||||
task = self.get(task)
|
||||
|
@ -18,22 +18,22 @@ from adjutant.tasks.v1.base import BaseTask
|
||||
class CreateProjectAndUser(BaseTask):
|
||||
duplicate_policy = "block"
|
||||
task_type = "create_project_and_user"
|
||||
deprecated_task_types = ['create_project', 'signup']
|
||||
deprecated_task_types = ["create_project", "signup"]
|
||||
default_actions = [
|
||||
"NewProjectWithUserAction",
|
||||
]
|
||||
|
||||
email_config = {
|
||||
'initial': {
|
||||
'template': 'create_project_and_user_initial.txt',
|
||||
'subject': 'signup received'
|
||||
"initial": {
|
||||
"template": "create_project_and_user_initial.txt",
|
||||
"subject": "signup received",
|
||||
},
|
||||
'token': {
|
||||
'template': 'create_project_and_user_token.txt',
|
||||
'subject': 'signup approved'
|
||||
"token": {
|
||||
"template": "create_project_and_user_token.txt",
|
||||
"subject": "signup approved",
|
||||
},
|
||||
"completed": {
|
||||
"template": "create_project_and_user_completed.txt",
|
||||
"subject": "signup completed",
|
||||
},
|
||||
'completed': {
|
||||
'template': 'create_project_and_user_completed.txt',
|
||||
'subject': 'signup completed'
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,10 @@ class UpdateProjectQuotas(BaseTask):
|
||||
]
|
||||
|
||||
email_config = {
|
||||
'initial': None,
|
||||
'token': None,
|
||||
'completed': {
|
||||
'template': 'create_project_and_user_completed.txt',
|
||||
'subject': 'signup completed'
|
||||
}
|
||||
"initial": None,
|
||||
"token": None,
|
||||
"completed": {
|
||||
"template": "create_project_and_user_completed.txt",
|
||||
"subject": "signup completed",
|
||||
},
|
||||
}
|
||||
|
@ -18,84 +18,80 @@ from adjutant.tasks.v1.base import BaseTask
|
||||
class InviteUser(BaseTask):
|
||||
duplicate_policy = "block"
|
||||
task_type = "invite_user_to_project"
|
||||
deprecated_task_types = ['invite_user']
|
||||
deprecated_task_types = ["invite_user"]
|
||||
default_actions = [
|
||||
"NewUserAction",
|
||||
]
|
||||
|
||||
email_config = {
|
||||
'initial': None,
|
||||
'token': {
|
||||
'template': 'invite_user_to_project_token.txt',
|
||||
'subject': 'invite_user_to_project'
|
||||
"initial": None,
|
||||
"token": {
|
||||
"template": "invite_user_to_project_token.txt",
|
||||
"subject": "invite_user_to_project",
|
||||
},
|
||||
"completed": {
|
||||
"template": "invite_user_to_project_completed.txt",
|
||||
"subject": "invite_user_to_project",
|
||||
},
|
||||
'completed': {
|
||||
'template': 'invite_user_to_project_completed.txt',
|
||||
'subject': 'invite_user_to_project'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ResetUserPassword(BaseTask):
|
||||
task_type = "reset_user_password"
|
||||
deprecated_task_types = ['reset_password']
|
||||
deprecated_task_types = ["reset_password"]
|
||||
default_actions = [
|
||||
"ResetUserPasswordAction",
|
||||
]
|
||||
|
||||
email_config = {
|
||||
'initial': None,
|
||||
'token': {
|
||||
'template': 'reset_user_password_token.txt',
|
||||
'subject': 'Password Reset for OpenStack'
|
||||
"initial": None,
|
||||
"token": {
|
||||
"template": "reset_user_password_token.txt",
|
||||
"subject": "Password Reset for OpenStack",
|
||||
},
|
||||
"completed": {
|
||||
"template": "reset_user_password_completed.txt",
|
||||
"subject": "Password Reset for OpenStack",
|
||||
},
|
||||
'completed': {
|
||||
'template': 'reset_user_password_completed.txt',
|
||||
'subject': 'Password Reset for OpenStack'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class EditUserRoles(BaseTask):
|
||||
task_type = "edit_user_roles"
|
||||
deprecated_task_types = ['edit_user']
|
||||
deprecated_task_types = ["edit_user"]
|
||||
default_actions = [
|
||||
"EditUserRolesAction",
|
||||
]
|
||||
|
||||
email_config = {
|
||||
'initial': None,
|
||||
'token': None,
|
||||
'completed': None
|
||||
}
|
||||
email_config = {"initial": None, "token": None, "completed": None}
|
||||
|
||||
|
||||
class UpdateUserEmail(BaseTask):
|
||||
task_type = "update_user_email"
|
||||
deprecated_task_types = ['update_email']
|
||||
deprecated_task_types = ["update_email"]
|
||||
default_actions = [
|
||||
"UpdateUserEmailAction",
|
||||
]
|
||||
additional_actions = [
|
||||
'SendAdditionalEmailAction',
|
||||
"SendAdditionalEmailAction",
|
||||
]
|
||||
action_config = {
|
||||
'SendAdditionalEmailAction': {
|
||||
'initial': {
|
||||
'subject': 'OpenStack Email Update Requested',
|
||||
'template': 'update_user_email_started.txt',
|
||||
'email_current_user': True,
|
||||
"SendAdditionalEmailAction": {
|
||||
"initial": {
|
||||
"subject": "OpenStack Email Update Requested",
|
||||
"template": "update_user_email_started.txt",
|
||||
"email_current_user": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
email_config = {
|
||||
'initial': None,
|
||||
'token': {
|
||||
'subject': 'update_user_email_token',
|
||||
'template': 'update_user_email_token.txt'
|
||||
"initial": None,
|
||||
"token": {
|
||||
"subject": "update_user_email_token",
|
||||
"template": "update_user_email_token.txt",
|
||||
},
|
||||
"completed": {
|
||||
"subject": "Email Update Complete",
|
||||
"template": "update_user_email_completed.txt",
|
||||
},
|
||||
'completed': {
|
||||
'subject': 'Email Update Complete',
|
||||
'template': 'update_user_email_completed.txt'
|
||||
}
|
||||
}
|
||||
|
@ -27,18 +27,21 @@ from adjutant.config import CONF
|
||||
from adjutant import exceptions
|
||||
|
||||
|
||||
LOG = getLogger('adjutant')
|
||||
LOG = getLogger("adjutant")
|
||||
|
||||
|
||||
def handle_task_error(e, task, error_text="while running task"):
|
||||
import traceback
|
||||
trace = traceback.format_exc()
|
||||
LOG.critical((
|
||||
"(%s) - Exception escaped! %s\nTrace: \n%s") % (
|
||||
timezone.now(), e, trace))
|
||||
|
||||
notes = ["Error: %s(%s) %s. See task itself for details."
|
||||
% (type(e).__name__, e, error_text)]
|
||||
trace = traceback.format_exc()
|
||||
LOG.critical(
|
||||
("(%s) - Exception escaped! %s\nTrace: \n%s") % (timezone.now(), e, trace)
|
||||
)
|
||||
|
||||
notes = [
|
||||
"Error: %s(%s) %s. See task itself for details."
|
||||
% (type(e).__name__, e, error_text)
|
||||
]
|
||||
|
||||
raise exceptions.TaskActionsFailed(task, internal_message=notes)
|
||||
|
||||
@ -49,11 +52,7 @@ def create_token(task, expiry_time=None):
|
||||
expire = timezone.now() + timedelta(seconds=expiry_time)
|
||||
|
||||
uuid = uuid4().hex
|
||||
token = Token.objects.create(
|
||||
task=task,
|
||||
token=uuid,
|
||||
expires=expire
|
||||
)
|
||||
token = Token.objects.create(task=task, token=uuid, expires=expire)
|
||||
token.save()
|
||||
return token
|
||||
|
||||
@ -63,13 +62,13 @@ def send_stage_email(task, email_conf, token=None):
|
||||
return
|
||||
|
||||
text_template = loader.get_template(
|
||||
email_conf['template'],
|
||||
using='include_etc_templates')
|
||||
html_template = email_conf['html_template']
|
||||
email_conf["template"], using="include_etc_templates"
|
||||
)
|
||||
html_template = email_conf["html_template"]
|
||||
if html_template:
|
||||
html_template = loader.get_template(
|
||||
html_template,
|
||||
using='include_etc_templates')
|
||||
html_template, using="include_etc_templates"
|
||||
)
|
||||
|
||||
emails = set()
|
||||
actions = {}
|
||||
@ -86,74 +85,62 @@ def send_stage_email(task, email_conf, token=None):
|
||||
|
||||
if len(emails) > 1:
|
||||
notes = {
|
||||
'errors':
|
||||
("Error: Unable to send update, more than one email for task: %s"
|
||||
% task.uuid)
|
||||
"errors": (
|
||||
"Error: Unable to send update, more than one email for task: %s"
|
||||
% task.uuid
|
||||
)
|
||||
}
|
||||
create_notification(task, notes, error=True)
|
||||
return
|
||||
|
||||
context = {
|
||||
'task': task,
|
||||
'actions': actions
|
||||
}
|
||||
context = {"task": task, "actions": actions}
|
||||
if token:
|
||||
tokenurl = CONF.workflow.horizon_url
|
||||
if not tokenurl.endswith('/'):
|
||||
tokenurl += '/'
|
||||
tokenurl += 'token/'
|
||||
context.update({
|
||||
'tokenurl': tokenurl,
|
||||
'token': token.token
|
||||
})
|
||||
if not tokenurl.endswith("/"):
|
||||
tokenurl += "/"
|
||||
tokenurl += "token/"
|
||||
context.update({"tokenurl": tokenurl, "token": token.token})
|
||||
|
||||
try:
|
||||
message = text_template.render(context)
|
||||
|
||||
# from_email is the return-path and is distinct from the
|
||||
# message headers
|
||||
from_email = email_conf['from']
|
||||
from_email = email_conf["from"]
|
||||
if not from_email:
|
||||
from_email = email_conf['reply']
|
||||
from_email = email_conf["reply"]
|
||||
elif "%(task_uuid)s" in from_email:
|
||||
from_email = from_email % {'task_uuid': task.uuid}
|
||||
from_email = from_email % {"task_uuid": task.uuid}
|
||||
|
||||
# these are the message headers which will be visible to
|
||||
# the email client.
|
||||
headers = {
|
||||
'X-Adjutant-Task-UUID': task.uuid,
|
||||
"X-Adjutant-Task-UUID": task.uuid,
|
||||
# From needs to be set to be disctinct from return-path
|
||||
'From': email_conf['reply'],
|
||||
'Reply-To': email_conf['reply'],
|
||||
"From": email_conf["reply"],
|
||||
"Reply-To": email_conf["reply"],
|
||||
}
|
||||
|
||||
email = EmailMultiAlternatives(
|
||||
email_conf['subject'],
|
||||
message,
|
||||
from_email,
|
||||
[emails.pop()],
|
||||
headers=headers,
|
||||
email_conf["subject"], message, from_email, [emails.pop()], headers=headers,
|
||||
)
|
||||
|
||||
if html_template:
|
||||
email.attach_alternative(
|
||||
html_template.render(context), "text/html")
|
||||
email.attach_alternative(html_template.render(context), "text/html")
|
||||
|
||||
email.send(fail_silently=False)
|
||||
|
||||
except Exception as e:
|
||||
notes = {
|
||||
'errors':
|
||||
("Error: '%s' while emailing update for task: %s" %
|
||||
(e, task.uuid))
|
||||
"errors": (
|
||||
"Error: '%s' while emailing update for task: %s" % (e, task.uuid)
|
||||
)
|
||||
}
|
||||
|
||||
notif_conf = task.config.notifications
|
||||
|
||||
if e.__class__.__name__ in notif_conf.safe_errors:
|
||||
notification = create_notification(
|
||||
task, notes, error=True,
|
||||
handlers=False)
|
||||
notification = create_notification(task, notes, error=True, handlers=False)
|
||||
notification.acknowledged = True
|
||||
notification.save()
|
||||
else:
|
||||
|
@ -15,5 +15,5 @@
|
||||
from django.conf.urls import include, url
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^', include('adjutant.api.urls')),
|
||||
url(r"^", include("adjutant.api.urls")),
|
||||
]
|
||||
|
@ -38,14 +38,14 @@ application = get_wsgi_application()
|
||||
# the Keystone Auth Middleware.
|
||||
conf = {
|
||||
"auth_plugin": "password",
|
||||
'username': CONF.identity.auth.username,
|
||||
'password': CONF.identity.auth.password,
|
||||
'project_name': CONF.identity.auth.project_name,
|
||||
"username": CONF.identity.auth.username,
|
||||
"password": CONF.identity.auth.password,
|
||||
"project_name": CONF.identity.auth.project_name,
|
||||
"project_domain_id": CONF.identity.auth.project_domain_id,
|
||||
"user_domain_id": CONF.identity.auth.user_domain_id,
|
||||
"auth_url": CONF.identity.auth.auth_url,
|
||||
'delay_auth_decision': True,
|
||||
'include_service_catalog': False,
|
||||
'token_cache_time': CONF.identity.token_cache_time,
|
||||
"delay_auth_decision": True,
|
||||
"include_service_catalog": False,
|
||||
"token_cache_time": CONF.identity.token_cache_time,
|
||||
}
|
||||
application = AuthProtocol(application, conf)
|
||||
|
@ -29,12 +29,9 @@
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'os_api_ref',
|
||||
'openstackdocstheme'
|
||||
]
|
||||
extensions = ["os_api_ref", "openstackdocstheme"]
|
||||
|
||||
html_theme = 'openstackdocstheme'
|
||||
html_theme = "openstackdocstheme"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
@ -42,37 +39,37 @@ html_theme = 'openstackdocstheme'
|
||||
# html_theme_options = {}
|
||||
|
||||
# openstackdocstheme settings
|
||||
repository_name = 'openstack/adjutant'
|
||||
html_theme = 'openstackdocs'
|
||||
repository_name = "openstack/adjutant"
|
||||
html_theme = "openstackdocs"
|
||||
use_storyboard = True
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = sphinx_bootstrap_theme.get_html_theme_path()
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u'Adjutant API Reference'
|
||||
copyright = u'2017, Catalyst IT Ltd'
|
||||
project = "Adjutant API Reference"
|
||||
copyright = "2017, Catalyst IT Ltd"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = ''
|
||||
version = ""
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = ''
|
||||
release = ""
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@ -86,7 +83,7 @@ release = ''
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
exclude_patterns = ["_build"]
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
@ -104,7 +101,7 @@ exclude_patterns = ['_build']
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
@ -195,7 +192,7 @@ pygments_style = 'sphinx'
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'AdjutantAPIReferencedoc'
|
||||
htmlhelp_basename = "AdjutantAPIReferencedoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
@ -215,9 +212,13 @@ htmlhelp_basename = 'AdjutantAPIReferencedoc'
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'AdjutantAPIReference.tex',
|
||||
u'Adjutant API Reference Documentation',
|
||||
u'Catalyst IT Ltd', 'manual'),
|
||||
(
|
||||
"index",
|
||||
"AdjutantAPIReference.tex",
|
||||
"Adjutant API Reference Documentation",
|
||||
"Catalyst IT Ltd",
|
||||
"manual",
|
||||
),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@ -246,8 +247,13 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'adjutantapireference', u'Adjutant API Reference Documentation',
|
||||
[u'Catalyst IT Ltd'], 1)
|
||||
(
|
||||
"index",
|
||||
"adjutantapireference",
|
||||
"Adjutant API Reference Documentation",
|
||||
["Catalyst IT Ltd"],
|
||||
1,
|
||||
)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
@ -260,12 +266,17 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'AdjutantAPIReference', u'Adjutant API Reference Documentation',
|
||||
u'Catalyst IT Ltd', 'AdjutantAPIReference',
|
||||
'A simple workflow framework to help automate admin and user tasks in '
|
||||
'and around OpenStack via a pluggable API exposing tasks made up of '
|
||||
'easily chainable actions.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
"index",
|
||||
"AdjutantAPIReference",
|
||||
"Adjutant API Reference Documentation",
|
||||
"Catalyst IT Ltd",
|
||||
"AdjutantAPIReference",
|
||||
"A simple workflow framework to help automate admin and user tasks in "
|
||||
"and around OpenStack via a pluggable API exposing tasks made up of "
|
||||
"easily chainable actions.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
@ -282,4 +293,4 @@ texinfo_documents = [
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'http://docs.python.org/': None}
|
||||
intersphinx_mapping = {"http://docs.python.org/": None}
|
||||
|
@ -30,30 +30,28 @@
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'openstackdocstheme'
|
||||
]
|
||||
extensions = ["openstackdocstheme"]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# openstackdocstheme settings
|
||||
repository_name = 'openstack/adjutant'
|
||||
html_theme = 'openstackdocs'
|
||||
repository_name = "openstack/adjutant"
|
||||
html_theme = "openstackdocs"
|
||||
use_storyboard = True
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u'Adjutant'
|
||||
copyright = u'2017, Catalyst IT Ltd'
|
||||
project = "Adjutant"
|
||||
copyright = "2017, Catalyst IT Ltd"
|
||||
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
@ -62,7 +60,7 @@ copyright = u'2017, Catalyst IT Ltd'
|
||||
exclude_patterns = []
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
@ -93,7 +91,7 @@ todo_include_todos = False
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Adjutantdoc'
|
||||
htmlhelp_basename = "Adjutantdoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
@ -102,8 +100,7 @@ htmlhelp_basename = 'Adjutantdoc'
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Adjutant.tex', u'Adjutant Documentation',
|
||||
u'Catalyst IT Ltd', 'manual'),
|
||||
(master_doc, "Adjutant.tex", "Adjutant Documentation", "Catalyst IT Ltd", "manual"),
|
||||
]
|
||||
|
||||
|
||||
@ -111,10 +108,7 @@ latex_documents = [
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'adjutant', u'Adjutant Documentation',
|
||||
['Catalyst IT Ltd'], 1)
|
||||
]
|
||||
man_pages = [(master_doc, "adjutant", "Adjutant Documentation", ["Catalyst IT Ltd"], 1)]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
@ -123,7 +117,13 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Adjutant', u'Adjutant Documentation',
|
||||
'Catalyst IT Ltd', 'Adjutant', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
master_doc,
|
||||
"Adjutant",
|
||||
"Adjutant Documentation",
|
||||
"Catalyst IT Ltd",
|
||||
"Adjutant",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
]
|
||||
|
@ -34,37 +34,37 @@
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'openstackdocstheme',
|
||||
'reno.sphinxext',
|
||||
"openstackdocstheme",
|
||||
"reno.sphinxext",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u'Adjutant Release Notes'
|
||||
copyright = u'2019, Adjutant Developers'
|
||||
project = "Adjutant Release Notes"
|
||||
copyright = "2019, Adjutant Developers"
|
||||
|
||||
# openstackdocstheme settings
|
||||
repository_name = 'openstack/adjutant'
|
||||
repository_name = "openstack/adjutant"
|
||||
use_storyboard = True
|
||||
|
||||
# Release notes are version independent
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
|
||||
release = ''
|
||||
release = ""
|
||||
# The short X.Y version.
|
||||
version = ''
|
||||
version = ""
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@ -96,7 +96,7 @@ exclude_patterns = []
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
@ -109,7 +109,7 @@ pygments_style = 'sphinx'
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'openstackdocs'
|
||||
html_theme = "openstackdocs"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
@ -188,7 +188,7 @@ html_theme = 'openstackdocs'
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'AdjutantReleaseNotesdoc'
|
||||
htmlhelp_basename = "AdjutantReleaseNotesdoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
@ -197,9 +197,13 @@ htmlhelp_basename = 'AdjutantReleaseNotesdoc'
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'AdjutantReleaseNotes.tex',
|
||||
u'Adjutant Release Notes Documentation',
|
||||
u'Adjutant Developers', 'manual'),
|
||||
(
|
||||
"index",
|
||||
"AdjutantReleaseNotes.tex",
|
||||
"Adjutant Release Notes Documentation",
|
||||
"Adjutant Developers",
|
||||
"manual",
|
||||
),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@ -228,8 +232,13 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'adjutantreleasenotes', u'Adjutant Release Notes Documentation',
|
||||
[u'Adjutant Developers'], 1)
|
||||
(
|
||||
"index",
|
||||
"adjutantreleasenotes",
|
||||
"Adjutant Release Notes Documentation",
|
||||
["Adjutant Developers"],
|
||||
1,
|
||||
)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
@ -242,10 +251,15 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'AdjutantReleaseNotes', u'Adjutant Release Notes Documentation',
|
||||
u'Adjutant Developers', 'AdjutantReleaseNotes',
|
||||
'An extensible API framework for admin logic in OpenStack.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
"index",
|
||||
"AdjutantReleaseNotes",
|
||||
"Adjutant Release Notes Documentation",
|
||||
"Adjutant Developers",
|
||||
"AdjutantReleaseNotes",
|
||||
"An extensible API framework for admin logic in OpenStack.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
@ -261,4 +275,4 @@ texinfo_documents = [
|
||||
# texinfo_no_detailmenu = False
|
||||
|
||||
# -- Options for Internationalization output ------------------------------
|
||||
locale_dirs = ['locale/']
|
||||
locale_dirs = ["locale/"]
|
||||
|
3
setup.py
3
setup.py
@ -15,6 +15,5 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
setup_requires=['pbr'],
|
||||
pbr=True,
|
||||
setup_requires=["pbr"], pbr=True,
|
||||
)
|
||||
|
2
tox.ini
2
tox.ini
@ -52,7 +52,7 @@ commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenote
|
||||
[flake8]
|
||||
max-line-length = 88
|
||||
select = C,E,F,W,B,B950
|
||||
ignore = D100,D101,D102,D103,D104,D105,D200,D203,D202,D204,D205,D208,D400,D401,W503,E501
|
||||
ignore = D100,D101,D102,D103,D104,D105,D200,D203,D202,D204,D205,D208,D400,D401,W503,E231,E501
|
||||
show-source = true
|
||||
builtins = _
|
||||
exclude=.venv,venv,.env,env,.git,.tox,dist,doc,*lib/python*,*egg,releasenotes,adjutant/api/migrations/*,adjutant/actions/migrations,adjutant/tasks/migrations
|
||||
|
Loading…
x
Reference in New Issue
Block a user