Reformat code using the Black style linter

Change-Id: I55cf016fe073e92fe4466f38b95ebdcd9ec58e84
This commit is contained in:
Adrian Turjak 2020-02-21 14:08:40 +13:00
parent 592e24170e
commit 2c62daf542
77 changed files with 5114 additions and 4820 deletions

View File

@ -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"
),
),
],
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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"
),
),
]

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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":

View File

@ -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

View File

@ -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

View File

@ -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)
)

View File

@ -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))

View File

@ -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"
),
),
]

View File

@ -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),
),
]

View File

@ -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={}),
),
]

View File

@ -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),
),
]

View File

@ -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",),
],
),
]

View File

@ -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"
),
),
]

View File

@ -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"
),
),
]

View File

@ -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",),],
),
]

View File

@ -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,
}

View File

@ -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))

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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()))

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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()

View File

@ -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

View File

@ -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 "

View File

@ -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",
],
)
)

View File

@ -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",
)
)

View File

@ -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"],

View File

@ -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"]},
)
)

View File

@ -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"],
)
)

View File

@ -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,

View File

@ -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."

View File

@ -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__)

View File

@ -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

View File

@ -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:

View File

@ -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 = {}

View File

@ -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.",
),
]
)

View File

@ -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)

View File

@ -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,
},
},
}

View File

@ -1 +1 @@
default_app_config = 'adjutant.startup.config.StartUpConfig'
default_app_config = "adjutant.startup.config.StartUpConfig"

View File

@ -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
)

View File

@ -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()

View File

@ -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": [],},
),
],
),

View File

@ -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",
),
),
]

View File

@ -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,

View File

@ -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")

View File

@ -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)

View File

@ -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'
}
}

View File

@ -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",
},
}

View File

@ -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'
}
}

View File

@ -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:

View File

@ -15,5 +15,5 @@
from django.conf.urls import include, url
urlpatterns = [
url(r'^', include('adjutant.api.urls')),
url(r"^", include("adjutant.api.urls")),
]

View File

@ -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)

View File

@ -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}

View File

@ -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",
),
]

View File

@ -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/"]

View File

@ -15,6 +15,5 @@
from setuptools import setup
setup(
setup_requires=['pbr'],
pbr=True,
setup_requires=["pbr"], pbr=True,
)

View File

@ -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