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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('api', '0001_initial'), ("api", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Action', name="Action",
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
('action_name', models.CharField(max_length=200)), "id",
('action_data', jsonfield.fields.JSONField(default={})), models.AutoField(
('cache', jsonfield.fields.JSONField(default={})), verbose_name="ID",
('state', models.CharField(default=b'default', max_length=200)), serialize=False,
('valid', models.BooleanField(default=False)), auto_created=True,
('need_token', models.BooleanField(default=False)), primary_key=True,
('order', models.IntegerField()), ),
('created', models.DateTimeField(default=django.utils.timezone.now)), ),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Task')), ("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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('actions', '0001_initial'), ("actions", "0001_initial"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='action', model_name="action",
name='auto_approve', name="auto_approve",
field=models.NullBooleanField(default=None), field=models.NullBooleanField(default=None),
), ),
] ]

View File

@ -8,13 +8,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('actions', '0002_action_auto_approve'), ("actions", "0002_action_auto_approve"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='action', model_name="action",
name='state', name="state",
field=models.CharField(default='default', max_length=200), field=models.CharField(default="default", max_length=200),
), ),
] ]

View File

@ -9,14 +9,16 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('tasks', '0001_initial'), ("tasks", "0001_initial"),
('actions', '0003_auto_20190610_0205'), ("actions", "0003_auto_20190610_0205"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='action', model_name="action",
name='task', name="task",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.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. Database model representation of an action.
""" """
action_name = models.CharField(max_length=200) action_name = models.CharField(max_length=200)
action_data = JSONField(default={}) action_data = JSONField(default={})
cache = JSONField(default={}) cache = JSONField(default={})
state = models.CharField(max_length=200, default="default") state = models.CharField(max_length=200, default="default")
valid = models.BooleanField(default=False) valid = models.BooleanField(default=False)
need_token = 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 # NOTE(amelia): Auto approve is technically a ternary operator
# If all in a task are None it will not auto approve # If all in a task are None it will not auto approve
# However if at least one action has it set to True it # However if at least one action has it set to True it
@ -46,5 +47,4 @@ class Action(models.Model):
def get_action(self): def get_action(self):
"""Returns self as the appropriate action wrapper type.""" """Returns self as the appropriate action wrapper type."""
data = self.action_data data = self.action_data
return actions.ACTION_CLASSES[self.action_name]( return actions.ACTION_CLASSES[self.action_name](data=data, action_model=self)
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 Function for sending emails from actions
""" """
if not conf.get('template'): if not conf.get("template"):
return return
if not to_addresses: if not to_addresses:
@ -40,66 +40,58 @@ def send_email(to_addresses, context, conf, task):
elif isinstance(to_addresses, set): elif isinstance(to_addresses, set):
to_addresses = list(to_addresses) to_addresses = list(to_addresses)
text_template = loader.get_template( text_template = loader.get_template(conf["template"], using="include_etc_templates")
conf['template'],
using='include_etc_templates')
html_template = conf.get('html_template') html_template = conf.get("html_template")
if html_template: if html_template:
html_template = loader.get_template( html_template = loader.get_template(
html_template, html_template, using="include_etc_templates"
using='include_etc_templates') )
try: try:
message = text_template.render(context) message = text_template.render(context)
# from_email is the return-path and is distinct from the # from_email is the return-path and is distinct from the
# message headers # message headers
from_email = conf.get('from') from_email = conf.get("from")
if not from_email: if not from_email:
from_email = conf.get('reply') from_email = conf.get("reply")
if not from_email: if not from_email:
return return
elif "%(task_uuid)s" in from_email: 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 # these are the message headers which will be visible to
# the email client. # the email client.
headers = { 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 needs to be set to be distinct from return-path
'From': reply_email, "From": reply_email,
'Reply-To': reply_email, "Reply-To": reply_email,
} }
email = EmailMultiAlternatives( email = EmailMultiAlternatives(
conf['subject'], conf["subject"], message, from_email, to_addresses, headers=headers,
message,
from_email,
to_addresses,
headers=headers,
) )
if html_template: if html_template:
email.attach_alternative( email.attach_alternative(html_template.render(context), "text/html")
html_template.render(context), "text/html")
email.send(fail_silently=False) email.send(fail_silently=False)
return True return True
except Exception as e: except Exception as e:
notes = { notes = {
'errors': "errors": (
("Error: '%s' while sending additional email for task: %s" % "Error: '%s' while sending additional email for task: %s"
(e, task.uuid)) % (e, task.uuid)
)
} }
notif_conf = task.config.notifications notif_conf = task.config.notifications
if e.__class__.__name__ in notif_conf.safe_errors: if e.__class__.__name__ in notif_conf.safe_errors:
notification = create_notification( notification = create_notification(task, notes, error=True, handlers=False)
task, notes, error=True,
handlers=False)
notification.acknowledged = True notification.acknowledged = True
notification.save() notification.save()
else: else:

View File

@ -64,15 +64,14 @@ class BaseAction(object):
config_group = None config_group = None
def __init__(self, data, action_model=None, task=None, def __init__(self, data, action_model=None, task=None, order=None):
order=None):
""" """
Build itself around an existing database model, Build itself around an existing database model,
or build itself and creates a new database model. or build itself and creates a new database model.
Sets up required data as fields. Sets up required data as fields.
""" """
self.logger = getLogger('adjutant') self.logger = getLogger("adjutant")
for field in self.required: for field in self.required:
field_data = data[field] field_data = data[field]
@ -86,7 +85,7 @@ class BaseAction(object):
action_name=self.__class__.__name__, action_name=self.__class__.__name__,
action_data=data, action_data=data,
task=task, task=task,
order=order order=order,
) )
action.save() action.save()
self.action = action self.action = action
@ -141,8 +140,7 @@ class BaseAction(object):
now = timezone.now() now = timezone.now()
self.logger.info("(%s) - %s" % (now, note)) self.logger.info("(%s) - %s" % (now, note))
note = "%s - (%s)" % (note, now) note = "%s - (%s)" % (note, now)
self.action.task.add_action_note( self.action.task.add_action_note(str(self), note)
str(self), note)
@property @property
def config(self): def config(self):
@ -154,8 +152,7 @@ class BaseAction(object):
return self._config return self._config
try: try:
action_defaults = CONF.workflow.action_defaults.get( action_defaults = CONF.workflow.action_defaults.get(self.__class__.__name__)
self.__class__.__name__)
except KeyError: except KeyError:
self._config = {} self._config = {}
return self._config return self._config
@ -163,7 +160,8 @@ class BaseAction(object):
try: try:
task_conf = CONF.workflow.tasks[self.action.task.task_type] task_conf = CONF.workflow.tasks[self.action.task.task_type]
self._config = action_defaults.overlay( self._config = action_defaults.overlay(
task_conf.actions[self.__class__.__name__]) task_conf.actions[self.__class__.__name__]
)
except KeyError: except KeyError:
self._config = action_defaults self._config = action_defaults
return self._config return self._config
@ -174,7 +172,8 @@ class BaseAction(object):
except NotImplementedError: except NotImplementedError:
self.logger.warning( self.logger.warning(
"DEPRECATED: Action '_pre_approve' stage has been renamed " "DEPRECATED: Action '_pre_approve' stage has been renamed "
"to 'prepare'.") "to 'prepare'."
)
return self._pre_approve() return self._pre_approve()
def approve(self): def approve(self):
@ -183,7 +182,8 @@ class BaseAction(object):
except NotImplementedError: except NotImplementedError:
self.logger.warning( self.logger.warning(
"DEPRECATED: Action '_post_approve' stage has been renamed " "DEPRECATED: Action '_post_approve' stage has been renamed "
"to 'prepare'.") "to 'prepare'."
)
return self._post_approve() return self._post_approve()
def submit(self, token_data): def submit(self, token_data):
@ -208,16 +208,16 @@ class ResourceMixin(object):
def _validate_keystone_user_project_id(self): def _validate_keystone_user_project_id(self):
keystone_user = self.action.task.keystone_user keystone_user = self.action.task.keystone_user
if keystone_user['project_id'] != self.project_id: if keystone_user["project_id"] != self.project_id:
self.add_note('Project id does not match keystone user project.') self.add_note("Project id does not match keystone user project.")
return False return False
return True return True
def _validate_keystone_user_domain_id(self): def _validate_keystone_user_domain_id(self):
keystone_user = self.action.task.keystone_user keystone_user = self.action.task.keystone_user
if keystone_user['project_domain_id'] != self.domain_id: if keystone_user["project_domain_id"] != self.domain_id:
self.add_note('Domain id does not match keystone user domain.') self.add_note("Domain id does not match keystone user domain.")
return False return False
return True return True
@ -225,7 +225,7 @@ class ResourceMixin(object):
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
domain = id_manager.get_domain(self.domain_id) domain = id_manager.get_domain(self.domain_id)
if not domain: if not domain:
self.add_note('Domain does not exist.') self.add_note("Domain does not exist.")
return False return False
return True return True
@ -234,24 +234,23 @@ class ResourceMixin(object):
# Handle an edge_case where some actions set their # Handle an edge_case where some actions set their
# own project_id value. # own project_id value.
if not self.project_id: if not self.project_id:
self.add_note('No project_id given.') self.add_note("No project_id given.")
return False return False
# Now actually check the project exists. # Now actually check the project exists.
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
project = id_manager.get_project(self.project_id) project = id_manager.get_project(self.project_id)
if not project: if not project:
self.add_note('Project with id %s does not exist.' % self.add_note("Project with id %s does not exist." % self.project_id)
self.project_id)
return False 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 return True
def _validate_domain_name(self): def _validate_domain_name(self):
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
self.domain = id_manager.find_domain(self.domain_name) self.domain = id_manager.find_domain(self.domain_name)
if not self.domain: if not self.domain:
self.add_note('Domain does not exist.') self.add_note("Domain does not exist.")
return False return False
# also store the domain_id separately for later use # also store the domain_id separately for later use
self.domain_id = self.domain.id self.domain_id = self.domain.id
@ -262,9 +261,9 @@ class ResourceMixin(object):
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
v_region = id_manager.get_region(region) v_region = id_manager.get_region(region)
if not v_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 return False
self.add_note('Region: %s exists.' % region) self.add_note("Region: %s exists." % region)
return True return True
@ -278,16 +277,17 @@ class UserMixin(ResourceMixin):
self.user = id_manager.find_user(self.username, self.domain.id) self.user = id_manager.find_user(self.username, self.domain.id)
if not self.user: if not self.user:
self.add_note('No user present with username') self.add_note("No user present with username")
return False return False
return True return True
def _validate_role_permissions(self): def _validate_role_permissions(self):
keystone_user = self.action.task.keystone_user keystone_user = self.action.task.keystone_user
# Role permissions check # Role permissions check
if not self.are_roles_manageable(user_roles=keystone_user['roles'], if not self.are_roles_manageable(
requested_roles=self.roles): user_roles=keystone_user["roles"], requested_roles=self.roles
self.add_note('User does not have permission to edit role(s).') ):
self.add_note("User does not have permission to edit role(s).")
return False return False
return True return True
@ -299,7 +299,7 @@ class UserMixin(ResourceMixin):
requested_roles = set(requested_roles) requested_roles = set(requested_roles)
# blacklist checks # blacklist checks
blacklisted_roles = set(['admin']) blacklisted_roles = set(["admin"])
if len(blacklisted_roles & requested_roles) > 0: if len(blacklisted_roles & requested_roles) > 0:
return False return False
@ -317,15 +317,16 @@ class UserMixin(ResourceMixin):
# Mutators # Mutators
def grant_roles(self, user, roles, project_id, inherited=False): def grant_roles(self, user, roles, project_id, inherited=False):
return self._user_roles_edit( 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): def remove_roles(self, user, roles, project_id, inherited=False):
return self._user_roles_edit( 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 # Helper function to add or remove roles
def _user_roles_edit(self, user, roles, project_id, remove=False, def _user_roles_edit(self, user, roles, project_id, remove=False, inherited=False):
inherited=False):
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
if not remove: if not remove:
action_fn = id_manager.add_user_role action_fn = id_manager.add_user_role
@ -346,8 +347,9 @@ class UserMixin(ResourceMixin):
action_fn(user, role, project_id, inherited=inherited) action_fn(user, role, project_id, inherited=inherited)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while %s the roles: %s on user: %s " % "Error: '%s' while %s the roles: %s on user: %s "
(e, action_string, roles, user)) % (e, action_string, roles, user)
)
raise raise
def enable_user(self, user=None): def enable_user(self, user=None):
@ -358,22 +360,27 @@ class UserMixin(ResourceMixin):
id_manager.enable_user(user) id_manager.enable_user(user)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while re-enabling user: %s with roles: %s" % "Error: '%s' while re-enabling user: %s with roles: %s"
(e, self.username, self.roles)) % (e, self.username, self.roles)
)
raise raise
def create_user(self, password): def create_user(self, password):
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
try: try:
user = id_manager.create_user( user = id_manager.create_user(
name=self.username, password=password, name=self.username,
email=self.email, domain=self.domain_id, password=password,
created_on=str_datetime(timezone.now())) email=self.email,
domain=self.domain_id,
created_on=str_datetime(timezone.now()),
)
except Exception as e: except Exception as e:
# TODO: Narrow the Exceptions caught to a relevant set. # TODO: Narrow the Exceptions caught to a relevant set.
self.add_note( self.add_note(
"Error: '%s' while creating user: %s with roles: %s" % "Error: '%s' while creating user: %s with roles: %s"
(e, self.username, self.roles)) % (e, self.username, self.roles)
)
raise raise
return user return user
@ -385,8 +392,8 @@ class UserMixin(ResourceMixin):
id_manager.update_user_password(user, password) id_manager.update_user_password(user, password)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while changing password for user: %s" % "Error: '%s' while changing password for user: %s" % (e, self.username)
(e, self.username)) )
raise raise
def update_email(self, email, user=None): def update_email(self, email, user=None):
@ -397,8 +404,8 @@ class UserMixin(ResourceMixin):
id_manager.update_user_email(user, email) id_manager.update_user_email(user, email)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while changing email for user: %s" % "Error: '%s' while changing email for user: %s" % (e, self.username)
(e, self.username)) )
raise raise
def update_user_name(self, username, user=None): def update_user_name(self, username, user=None):
@ -409,8 +416,8 @@ class UserMixin(ResourceMixin):
id_manager.update_user_name(user, username) id_manager.update_user_name(user, username)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while changing username for user: %s" % "Error: '%s' while changing username for user: %s" % (e, self.username)
(e, self.username)) )
raise raise
@ -424,22 +431,18 @@ class ProjectMixin(ResourceMixin):
if self.parent_id: if self.parent_id:
parent = id_manager.get_project(self.parent_id) parent = id_manager.get_project(self.parent_id)
if not parent: if not parent:
self.add_note("Parent id: '%s' does not exist." % self.add_note("Parent id: '%s' does not exist." % self.parent_id)
self.parent_id)
return False return False
return True return True
def _validate_project_absent(self): def _validate_project_absent(self):
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
project = id_manager.find_project( project = id_manager.find_project(self.project_name, self.domain_id)
self.project_name, self.domain_id)
if project: if project:
self.add_note("Existing project with name '%s'." % self.add_note("Existing project with name '%s'." % self.project_name)
self.project_name)
return False return False
self.add_note("No existing project with name '%s'." % self.add_note("No existing project with name '%s'." % self.project_name)
self.project_name)
return True return True
def _create_project(self): def _create_project(self):
@ -447,17 +450,20 @@ class ProjectMixin(ResourceMixin):
description = getattr(self, "description", "") description = getattr(self, "description", "")
try: try:
project = id_manager.create_project( project = id_manager.create_project(
self.project_name, created_on=str_datetime(timezone.now()), self.project_name,
parent=self.parent_id, domain=self.domain_id, created_on=str_datetime(timezone.now()),
description=description) parent=self.parent_id,
domain=self.domain_id,
description=description,
)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while creating project: %s" % "Error: '%s' while creating project: %s" % (e, self.project_name)
(e, self.project_name)) )
raise raise
# put project_id into action cache: # put project_id into action cache:
self.action.task.cache['project_id'] = project.id self.action.task.cache["project_id"] = project.id
self.set_cache('project_id', project.id) self.set_cache("project_id", project.id)
self.add_note("New project '%s' created." % project.name) self.add_note("New project '%s' created." % project.name)
@ -477,7 +483,8 @@ class QuotaMixin(ResourceMixin):
def _usage_greater_than_quota(self, regions): def _usage_greater_than_quota(self, regions):
quota_manager = QuotaManager( quota_manager = QuotaManager(
self.project_id, 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, {}) quota = CONF.quota.sizes.get(self.size, {})
for region in regions: for region in regions:
current_usage = quota_manager.get_current_usage(region) current_usage = quota_manager.get_current_usage(region)
@ -500,7 +507,6 @@ class QuotaMixin(ResourceMixin):
class UserIdAction(BaseAction): class UserIdAction(BaseAction):
def _get_target_user(self): def _get_target_user(self):
""" """
Gets the target user by id Gets the target user by id
@ -522,7 +528,7 @@ class UserNameAction(BaseAction):
# NOTE(amelia): Make a copy to avoid editing it globally. # NOTE(amelia): Make a copy to avoid editing it globally.
self.required = list(self.required) self.required = list(self.required)
try: try:
self.required.remove('username') self.required.remove("username")
except ValueError: except ValueError:
pass pass
# nothing to remove # nothing to remove

View File

@ -32,35 +32,40 @@ def _build_default_email_group(group_name):
fields.StrConfig( fields.StrConfig(
"subject", "subject",
help_text="Email subject for this stage.", help_text="Email subject for this stage.",
default="Openstack Email Notification") default="Openstack Email Notification",
)
) )
email_group.register_child_config( email_group.register_child_config(
fields.StrConfig( fields.StrConfig(
"from", "from",
help_text="From email for this stage.", help_text="From email for this stage.",
regex=constants.EMAIL_WITH_TEMPLATE_REGEX, 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( email_group.register_child_config(
fields.StrConfig( fields.StrConfig(
"reply", "reply",
help_text="Reply-to email for this stage.", help_text="Reply-to email for this stage.",
regex=constants.EMAIL_WITH_TEMPLATE_REGEX, regex=constants.EMAIL_WITH_TEMPLATE_REGEX,
default="no-reply@example.com") default="no-reply@example.com",
)
) )
email_group.register_child_config( email_group.register_child_config(
fields.StrConfig( fields.StrConfig(
"template", "template",
help_text="Email template for this stage. " help_text="Email template for this stage. "
"No template will cause the email not to send.", "No template will cause the email not to send.",
default=None) default=None,
)
) )
email_group.register_child_config( email_group.register_child_config(
fields.StrConfig( fields.StrConfig(
"html_template", "html_template",
help_text="Email html template for this stage. " help_text="Email html template for this stage. "
"No template will cause the email not to send.", "No template will cause the email not to send.",
default=None) default=None,
)
) )
email_group.register_child_config( email_group.register_child_config(
fields.BoolConfig( fields.BoolConfig(
@ -108,24 +113,27 @@ class SendAdditionalEmailAction(BaseAction):
def set_email(self, conf): def set_email(self, conf):
self.emails = set() 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") self.add_note("Adding the current user's email address")
if CONF.identity.username_is_email: 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: else:
try: try:
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
email = id_manager.get_user( 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) self.emails.add(email)
except AttributeError: except AttributeError:
self.add_note("Could not add current user email address") self.add_note("Could not add current user email address")
if conf.get('email_roles'): if conf.get("email_roles"):
roles = set(conf.get('email_roles')) roles = set(conf.get("email_roles"))
project_id = self.action.task.keystone_user['project_id'] project_id = self.action.task.keystone_user["project_id"]
self.add_note('Adding email addresses for roles %s in project %s' self.add_note(
% (roles, project_id)) "Adding email addresses for roles %s in project %s"
% (roles, project_id)
)
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
users = id_manager.list_users(project_id) users = id_manager.list_users(project_id)
@ -137,14 +145,14 @@ class SendAdditionalEmailAction(BaseAction):
else: else:
self.emails.add(user.email) self.emails.add(user.email)
if conf.get('email_task_cache'): if conf.get("email_task_cache"):
task_emails = self.action.task.cache.get('additional_emails', []) task_emails = self.action.task.cache.get("additional_emails", [])
if isinstance(task_emails, six.string_types): if isinstance(task_emails, six.string_types):
task_emails = [task_emails] task_emails = [task_emails]
for email in task_emails: for email in task_emails:
self.emails.add(email) self.emails.add(email)
for email in conf.get('email_additional_addresses'): for email in conf.get("email_additional_addresses"):
self.emails.add(email) self.emails.add(email)
def _validate(self): def _validate(self):
@ -152,13 +160,13 @@ class SendAdditionalEmailAction(BaseAction):
self.action.save() self.action.save()
def _prepare(self): def _prepare(self):
self.perform_action('prepare') self.perform_action("prepare")
def _approve(self): def _approve(self):
self.perform_action('approve') self.perform_action("approve")
def _submit(self, data): def _submit(self, data):
self.perform_action('submit') self.perform_action("submit")
def perform_action(self, stage): def perform_action(self, stage):
self._validate() self._validate()
@ -171,7 +179,7 @@ class SendAdditionalEmailAction(BaseAction):
email_conf = self.config.get(stage) email_conf = self.config.get(stage)
# If either of these are false we won't be sending anything. # 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 return
self.set_email(email_conf) self.set_email(email_conf)
@ -188,10 +196,7 @@ class SendAdditionalEmailAction(BaseAction):
act = action.get_action() act = action.get_action()
actions[str(act)] = act actions[str(act)] = act
context = { context = {"task": task, "actions": actions}
'task': task,
'actions': actions
}
result = send_email(self.emails, context, email_conf, task) 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 import user_store
from adjutant.common.utils import str_datetime from adjutant.common.utils import str_datetime
from adjutant.actions.utils import validate_steps from adjutant.actions.utils import validate_steps
from adjutant.actions.v1.base import ( from adjutant.actions.v1.base import BaseAction, UserNameAction, UserMixin, ProjectMixin
BaseAction, UserNameAction, UserMixin, ProjectMixin)
from adjutant.actions.v1 import serializers from adjutant.actions.v1 import serializers
@ -36,10 +35,10 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
""" """
required = [ required = [
'domain_id', "domain_id",
'parent_id', "parent_id",
'project_name', "project_name",
'description', "description",
] ]
serializer = serializers.NewProjectSerializer serializer = serializers.NewProjectSerializer
@ -50,7 +49,7 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
"default_roles", "default_roles",
help_text="Roles to be given on project to the creating user.", help_text="Roles to be given on project to the creating user.",
default=[], 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) super(NewProjectAction, self).__init__(*args, **kwargs)
def _validate(self): def _validate(self):
self.action.valid = validate_steps([ self.action.valid = validate_steps(
self._validate_domain_id, [
self._validate_keystone_user_parent_project, self._validate_domain_id,
self._validate_project_absent, self._validate_keystone_user_parent_project,
]) self._validate_project_absent,
]
)
self.action.save() self.action.save()
def _validate_domain_id(self): def _validate_domain_id(self):
keystone_user = self.action.task.keystone_user keystone_user = self.action.task.keystone_user
if keystone_user['project_domain_id'] != self.domain_id: if keystone_user["project_domain_id"] != self.domain_id:
self.add_note('Domain id does not match keystone user domain.') self.add_note("Domain id does not match keystone user domain.")
return False return False
return super(NewProjectAction, self)._validate_domain_id() return super(NewProjectAction, self)._validate_domain_id()
@ -79,9 +80,8 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
if self.parent_id: if self.parent_id:
keystone_user = self.action.task.keystone_user keystone_user = self.action.task.keystone_user
if self.parent_id != keystone_user['project_id']: if self.parent_id != keystone_user["project_id"]:
self.add_note( self.add_note("Parent id does not match keystone user project.")
'Parent id does not match keystone user project.')
return False return False
return self._validate_parent_project() return self._validate_parent_project()
return True return True
@ -90,9 +90,9 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
self._validate() self._validate()
def _approve(self): def _approve(self):
project_id = self.get_cache('project_id') project_id = self.get_cache("project_id")
if project_id: if project_id:
self.action.task.cache['project_id'] = project_id self.action.task.cache["project_id"] = project_id
self.add_note("Project already created.") self.add_note("Project already created.")
else: else:
self._validate() self._validate()
@ -102,34 +102,38 @@ class NewProjectAction(BaseAction, ProjectMixin, UserMixin):
self._create_project() self._create_project()
user_id = self.get_cache('user_id') user_id = self.get_cache("user_id")
if 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.") self.add_note("User already given roles.")
else: else:
default_roles = self.config.default_roles 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 keystone_user = self.action.task.keystone_user
try: try:
id_manager = user_store.IdentityManager() 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) self.grant_roles(user, default_roles, project_id)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
("Error: '%s' while adding roles %s " (
"to user '%s' on project '%s'") % "Error: '%s' while adding roles %s "
(e, default_roles, user.name, project_id)) "to user '%s' on project '%s'"
)
% (e, default_roles, user.name, project_id)
)
raise raise
# put user_id into action cache: # put user_id into action cache:
self.action.task.cache['user_id'] = user.id self.action.task.cache["user_id"] = user.id
self.set_cache('user_id', user.id) self.set_cache("user_id", user.id)
self.add_note( self.add_note(
"Existing user '%s' attached to project %s with roles: %s" "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): def _submit(self, token_data):
""" """
@ -144,13 +148,7 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
doesn't exists. doesn't exists.
""" """
required = [ required = ["domain_id", "parent_id", "project_name", "username", "email"]
'domain_id',
'parent_id',
'project_name',
'username',
'email'
]
serializer = serializers.NewProjectWithUserSerializer serializer = serializers.NewProjectWithUserSerializer
@ -160,7 +158,7 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
"default_roles", "default_roles",
help_text="Roles to be given on project for the user.", help_text="Roles to be given on project for the user.",
default=[], 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) super(NewProjectWithUserAction, self).__init__(*args, **kwargs)
def _validate(self): def _validate(self):
self.action.valid = validate_steps([ self.action.valid = validate_steps(
self._validate_domain_id, [
self._validate_parent_project, self._validate_domain_id,
self._validate_project_absent, self._validate_parent_project,
self._validate_user, self._validate_project_absent,
]) self._validate_user,
]
)
self.action.save() self.action.save()
def _validate_user(self): def _validate_user(self):
@ -184,36 +184,40 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
if not user: if not user:
self.add_note( self.add_note(
"No user present with username '%s'. " "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: if not id_manager.can_edit_users:
self.add_note( self.add_note(
'Identity backend does not support user editing, ' "Identity backend does not support user editing, "
'cannot create new user.') "cannot create new user."
)
return False return False
# add to cache to use in template # 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.action.need_token = True
self.set_token_fields(["password"]) self.set_token_fields(["password"])
return True return True
if (not CONF.identity.username_is_email if (
and getattr(user, 'email', None) != self.email): not CONF.identity.username_is_email
self.add_note("Existing user '%s' with non-matching email." % and getattr(user, "email", None) != self.email
self.username) ):
self.add_note("Existing user '%s' with non-matching email." % self.username)
return False return False
if not user.enabled: if not user.enabled:
self.add_note( self.add_note(
"Existing disabled user '%s' with matching email." % "Existing disabled user '%s' with matching email." % self.email
self.email) )
if not id_manager.can_edit_users: if not id_manager.can_edit_users:
self.add_note( self.add_note(
'Identity backend does not support user editing, ' "Identity backend does not support user editing, "
'cannot renable user.') "cannot renable user."
)
return False return False
self.action.state = "disabled" self.action.state = "disabled"
# add to cache to use in template # 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 self.action.need_token = True
# as they are disabled we'll reset their password # as they are disabled we'll reset their password
self.set_token_fields(["password"]) self.set_token_fields(["password"])
@ -221,15 +225,14 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
else: else:
self.action.state = "existing" self.action.state = "existing"
# add to cache to use in template # 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.action.need_token = False
self.add_note("Existing user '%s' with matching email." % self.add_note("Existing user '%s' with matching email." % self.email)
self.email)
return True return True
def _validate_user_submit(self): def _validate_user_submit(self):
user_id = self.get_cache('user_id') user_id = self.get_cache("user_id")
project_id = self.get_cache('project_id') project_id = self.get_cache("project_id")
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
@ -241,7 +244,7 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
else: else:
self.action.valid = False 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() self.action.save()
@ -257,15 +260,16 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
if not self.valid: if not self.valid:
return return
project_id = self.get_cache('project_id') project_id = self.get_cache("project_id")
if project_id: if project_id:
self.action.task.cache['project_id'] = project_id self.action.task.cache["project_id"] = project_id
self.add_note("Project already created.") self.add_note("Project already created.")
else: else:
self.action.valid = ( self.action.valid = (
self._validate_domain_id() self._validate_domain_id()
and self._validate_parent_project() and self._validate_parent_project()
and self._validate_project_absent()) and self._validate_project_absent()
)
self.action.save() self.action.save()
if not self.valid: if not self.valid:
@ -274,11 +278,11 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
self._create_project() self._create_project()
# User validation and checks # User validation and checks
user_id = self.get_cache('user_id') user_id = self.get_cache("user_id")
roles_granted = self.get_cache('roles_granted') roles_granted = self.get_cache("roles_granted")
if user_id and roles_granted: if user_id and roles_granted:
self.action.task.cache['user_id'] = user_id self.action.task.cache["user_id"] = user_id
self.action.task.cache['user_state'] = self.action.state self.action.task.cache["user_state"] = self.action.state
self.add_note("User already setup.") self.add_note("User already setup.")
elif not user_id: elif not user_id:
self.action.valid = self._validate_user() self.action.valid = self._validate_user()
@ -295,60 +299,66 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
default_roles = self.config.default_roles 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": if self.action.state == "default":
try: try:
# Generate a temporary password: # Generate a temporary password:
password = uuid4().hex + uuid4().hex password = uuid4().hex + uuid4().hex
user_id = self.get_cache('user_id') user_id = self.get_cache("user_id")
if not user_id: if not user_id:
user = id_manager.create_user( user = id_manager.create_user(
name=self.username, password=password, name=self.username,
email=self.email, domain=self.domain_id, password=password,
created_on=str_datetime(timezone.now())) email=self.email,
self.set_cache('user_id', user.id) domain=self.domain_id,
created_on=str_datetime(timezone.now()),
)
self.set_cache("user_id", user.id)
else: else:
user = id_manager.get_user(user_id) user = id_manager.get_user(user_id)
# put user_id into action cache: # 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) self.grant_roles(user, default_roles, project_id)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while creating user: %s with roles: %s" % "Error: '%s' while creating user: %s with roles: %s"
(e, self.username, default_roles)) % (e, self.username, default_roles)
)
raise raise
self.set_cache('roles_granted', True) self.set_cache("roles_granted", True)
self.add_note( self.add_note(
"New user '%s' created for project %s with roles: %s" % "New user '%s' created for project %s with roles: %s"
(self.username, project_id, default_roles)) % (self.username, project_id, default_roles)
)
elif self.action.state == "existing": elif self.action.state == "existing":
try: try:
user_id = self.get_cache('user_id') user_id = self.get_cache("user_id")
if not user_id: if not user_id:
user = id_manager.find_user( user = id_manager.find_user(self.username, self.domain_id)
self.username, self.domain_id) self.set_cache("user_id", user.id)
self.set_cache('user_id', user.id)
else: else:
user = id_manager.get_user(user_id) 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) self.grant_roles(user, default_roles, project_id)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while granting roles: %s to user: %s" % "Error: '%s' while granting roles: %s to user: %s"
(e, default_roles, self.username)) % (e, default_roles, self.username)
)
raise raise
self.set_cache('roles_granted', True) self.set_cache("roles_granted", True)
self.add_note( self.add_note(
"Existing user '%s' setup on project %s with roles: %s" "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": elif self.action.state == "disabled":
user_id = self.get_cache('user_id') user_id = self.get_cache("user_id")
if not user_id: if not user_id:
# first re-enable user # first re-enable user
try: try:
@ -356,8 +366,8 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
id_manager.enable_user(user) id_manager.enable_user(user)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while re-enabling user: %s" % "Error: '%s' while re-enabling user: %s" % (e, self.username)
(e, self.username)) )
raise raise
# and now update their password # and now update their password
@ -367,32 +377,34 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
id_manager.update_user_password(user, password) id_manager.update_user_password(user, password)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while changing password for user: %s" % "Error: '%s' while changing password for user: %s"
(e, self.username)) % (e, self.username)
)
raise raise
self.add_note( self.add_note("User %s password has been changed." % self.username)
'User %s password has been changed.' % self.username)
self.set_cache('user_id', user.id) self.set_cache("user_id", user.id)
else: else:
user = id_manager.get_user(user_id) 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 # now add their roles
roles_granted = self.get_cache('roles_granted') roles_granted = self.get_cache("roles_granted")
if not roles_granted: if not roles_granted:
try: try:
self.grant_roles(user, default_roles, project_id) self.grant_roles(user, default_roles, project_id)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while granting user: %s roles: %s" % "Error: '%s' while granting user: %s roles: %s"
(e, self.username, default_roles)) % (e, self.username, default_roles)
)
raise raise
self.set_cache('roles_granted', True) self.set_cache("roles_granted", True)
self.add_note( self.add_note(
"Existing user '%s' setup on project %s with roles: %s" "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): def _submit(self, token_data):
""" """
@ -407,29 +419,30 @@ class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin):
if not self.valid: if not self.valid:
return return
project_id = self.get_cache('project_id') project_id = self.get_cache("project_id")
self.action.task.cache['project_id'] = project_id self.action.task.cache["project_id"] = project_id
user_id = self.get_cache('user_id') user_id = self.get_cache("user_id")
self.action.task.cache['user_id'] = user_id self.action.task.cache["user_id"] = user_id
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
if self.action.state in ["default", "disabled"]: if self.action.state in ["default", "disabled"]:
user = id_manager.get_user(user_id) user = id_manager.get_user(user_id)
try: try:
id_manager.update_user_password( id_manager.update_user_password(user, token_data["password"])
user, token_data['password'])
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while changing password for user: %s" % "Error: '%s' while changing password for user: %s"
(e, self.username)) % (e, self.username)
)
raise 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": elif self.action.state == "existing":
# do nothing, everything is already done. # do nothing, everything is already done.
self.add_note( self.add_note(
"Existing user '%s' already attached to project %s" % ( "Existing user '%s' already attached to project %s"
user_id, project_id)) % (user_id, project_id)
)
class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin): class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
@ -441,7 +454,7 @@ class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
""" """
required = [ required = [
'domain_id', "domain_id",
] ]
serializer = serializers.AddDefaultUsersToProjectSerializer serializer = serializers.AddDefaultUsersToProjectSerializer
@ -472,24 +485,21 @@ class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
for user in self.users: for user in self.users:
ks_user = id_manager.find_user(user, self.domain_id) ks_user = id_manager.find_user(user, self.domain_id)
if ks_user: if ks_user:
self.add_note('User: %s exists.' % user) self.add_note("User: %s exists." % user)
else: else:
self.add_note('ERROR: User: %s does not exist.' % user) self.add_note("ERROR: User: %s does not exist." % user)
all_found = False all_found = False
return all_found return all_found
def _pre_validate(self): def _pre_validate(self):
self.action.valid = validate_steps([ self.action.valid = validate_steps([self._validate_users,])
self._validate_users,
])
self.action.save() self.action.save()
def _validate(self): def _validate(self):
self.action.valid = validate_steps([ self.action.valid = validate_steps(
self._validate_users, [self._validate_users, self._validate_project_id,]
self._validate_project_id, )
])
self.action.save() self.action.save()
def _prepare(self): def _prepare(self):
@ -497,7 +507,7 @@ class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin):
def _approve(self): def _approve(self):
id_manager = user_store.IdentityManager() 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() self._validate()
if self.valid and not self.action.state == "completed": 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.grant_roles(ks_user, self.roles, self.project_id)
self.add_note( self.add_note(
'User: "%s" given roles: %s on project: %s.' % 'User: "%s" given roles: %s on project: %s.'
(ks_user.name, self.roles, self.project_id)) % (ks_user.name, self.roles, self.project_id)
)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while adding users to project: %s" % "Error: '%s' while adding users to project: %s"
(e, self.project_id)) % (e, self.project_id)
)
raise raise
self.action.state = "completed" self.action.state = "completed"
self.action.save() self.action.save()

View File

@ -36,9 +36,9 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
""" """
required = [ required = [
'setup_network', "setup_network",
'project_id', "project_id",
'region', "region",
] ]
serializer = serializers.NewDefaultNetworkSerializer serializer = serializers.NewDefaultNetworkSerializer
@ -64,23 +64,20 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
default="default_router", default="default_router",
), ),
fields.StrConfig( fields.StrConfig(
"public_network", "public_network", help_text="ID of the public network.",
help_text="ID of the public network.",
), ),
fields.StrConfig( fields.StrConfig(
"subnet_cidr", "subnet_cidr", help_text="CIDR for the default subnet.",
help_text="CIDR for the default subnet.",
), ),
fields.ListConfig( fields.ListConfig(
"dns_nameservers", "dns_nameservers", help_text="DNS nameservers for the subnet.",
help_text="DNS nameservers for the subnet.",
), ),
] ],
), ),
fields.DictConfig( fields.DictConfig(
"regions", "regions",
help_text="Specific per region config for default network. " help_text="Specific per region config for default network. "
"See 'region_defaults'.", "See 'region_defaults'.",
default={}, default={},
), ),
] ]
@ -91,83 +88,86 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
def _validate_region(self): def _validate_region(self):
if not self.region: if not self.region:
self.add_note('ERROR: No region given.') self.add_note("ERROR: No region given.")
return False return False
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
region = id_manager.get_region(self.region) region = id_manager.get_region(self.region)
if not region: if not region:
self.add_note('ERROR: Region does not exist.') self.add_note("ERROR: Region does not exist.")
return False return False
self.add_note('Region: %s exists.' % self.region) self.add_note("Region: %s exists." % self.region)
return True return True
def _validate(self): def _validate(self):
self.action.valid = validate_steps([ self.action.valid = validate_steps(
self._validate_region, [
self._validate_project_id, self._validate_region,
self._validate_keystone_user_project_id, self._validate_project_id,
]) self._validate_keystone_user_project_id,
]
)
self.action.save() self.action.save()
def _create_network(self): def _create_network(self):
neutron = openstack_clients.get_neutronclient(region=self.region) neutron = openstack_clients.get_neutronclient(region=self.region)
try: try:
region_config = self.config.regions[self.region] region_config = self.config.regions[self.region]
network_config = self.config.region_defaults.overlay( network_config = self.config.region_defaults.overlay(region_config)
region_config)
except KeyError: except KeyError:
network_config = self.config.region_defaults network_config = self.config.region_defaults
if not self.get_cache('network_id'): if not self.get_cache("network_id"):
try: try:
network_body = { network_body = {
"network": { "network": {
"name": network_config.network_name, "name": network_config.network_name,
'tenant_id': self.project_id, "tenant_id": self.project_id,
"admin_state_up": True "admin_state_up": True,
} }
} }
network = neutron.create_network(body=network_body) network = neutron.create_network(body=network_body)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while creating network: %s" % "Error: '%s' while creating network: %s"
(e, network_config.network_name)) % (e, network_config.network_name)
)
raise raise
self.set_cache('network_id', network['network']['id']) self.set_cache("network_id", network["network"]["id"])
self.add_note("Network %s created for project %s" % self.add_note(
(network_config.network_name, "Network %s created for project %s"
self.project_id)) % (network_config.network_name, self.project_id)
)
else: else:
self.add_note("Network %s already created for project %s" % self.add_note(
(network_config.network_name, "Network %s already created for project %s"
self.project_id)) % (network_config.network_name, self.project_id)
)
if not self.get_cache('subnet_id'): if not self.get_cache("subnet_id"):
try: try:
subnet_body = { subnet_body = {
"subnet": { "subnet": {
"network_id": self.get_cache('network_id'), "network_id": self.get_cache("network_id"),
"ip_version": 4, "ip_version": 4,
'tenant_id': self.project_id, "tenant_id": self.project_id,
'dns_nameservers': network_config.dns_nameservers, "dns_nameservers": network_config.dns_nameservers,
"cidr": network_config.subnet_cidr "cidr": network_config.subnet_cidr,
} }
} }
subnet = neutron.create_subnet(body=subnet_body) subnet = neutron.create_subnet(body=subnet_body)
except Exception as e: except Exception as e:
self.add_note( self.add_note("Error: '%s' while creating subnet" % e)
"Error: '%s' while creating subnet" % e)
raise raise
self.set_cache('subnet_id', subnet['subnet']['id']) self.set_cache("subnet_id", subnet["subnet"]["id"])
self.add_note("Subnet created for network %s" % self.add_note("Subnet created for network %s" % network_config.network_name)
network_config.network_name)
else: else:
self.add_note("Subnet already created for network %s" % self.add_note(
network_config.network_name) "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: try:
router_body = { router_body = {
"router": { "router": {
@ -175,39 +175,35 @@ class NewDefaultNetworkAction(BaseAction, ProjectMixin):
"external_gateway_info": { "external_gateway_info": {
"network_id": network_config.public_network "network_id": network_config.public_network
}, },
'tenant_id': self.project_id, "tenant_id": self.project_id,
"admin_state_up": True "admin_state_up": True,
} }
} }
router = neutron.create_router(body=router_body) router = neutron.create_router(body=router_body)
except Exception as e: except Exception as e:
self.add_note( self.add_note(
"Error: '%s' while creating router: %s" % "Error: '%s' while creating router: %s"
(e, network_config.router_name)) % (e, network_config.router_name)
)
raise raise
self.set_cache('router_id', router['router']['id']) self.set_cache("router_id", router["router"]["id"])
self.add_note("Router created for project %s" % self.add_note("Router created for project %s" % self.project_id)
self.project_id)
else: else:
self.add_note("Router already created for project %s" % self.add_note("Router already created for project %s" % self.project_id)
self.project_id)
if not self.get_cache('port_id'): if not self.get_cache("port_id"):
try: try:
interface_body = { interface_body = {"subnet_id": self.get_cache("subnet_id")}
"subnet_id": self.get_cache('subnet_id')
}
interface = neutron.add_interface_router( 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: except Exception as e:
self.add_note( self.add_note("Error: '%s' while attaching interface" % e)
"Error: '%s' while attaching interface" % e)
raise 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") self.add_note("Interface added to router for subnet")
else: else:
self.add_note( self.add_note("Interface added to router for project %s" % self.project_id)
"Interface added to router for project %s" % self.project_id)
def _prepare(self): def _prepare(self):
# Note: Do we need to get this from cache? it is a required setting # Note: Do we need to get this from cache? it is a required setting
@ -231,31 +227,28 @@ class NewProjectDefaultNetworkAction(NewDefaultNetworkAction):
""" """
required = [ required = [
'setup_network', "setup_network",
'region', "region",
] ]
serializer = serializers.NewProjectDefaultNetworkSerializer serializer = serializers.NewProjectDefaultNetworkSerializer
def _pre_validate(self): def _pre_validate(self):
# Note: Don't check project here as it doesn't exist yet. # Note: Don't check project here as it doesn't exist yet.
self.action.valid = validate_steps([ self.action.valid = validate_steps([self._validate_region,])
self._validate_region,
])
self.action.save() self.action.save()
def _validate(self): def _validate(self):
self.action.valid = validate_steps([ self.action.valid = validate_steps(
self._validate_region, [self._validate_region, self._validate_project_id,]
self._validate_project_id, )
])
self.action.save() self.action.save()
def _prepare(self): def _prepare(self):
self._pre_validate() self._pre_validate()
def _approve(self): 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() self._validate()
if self.setup_network and self.valid: 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 """ """ Updates quota for a project to a given size in a list of regions """
required = [ required = [
'size', "size",
'project_id', "project_id",
'regions', "regions",
] ]
serializer = serializers.UpdateProjectQuotasSerializer serializer = serializers.UpdateProjectQuotasSerializer
@ -293,10 +286,10 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
def _get_email(self): def _get_email(self):
if CONF.identity.username_is_email: if CONF.identity.username_is_email:
return self.action.task.keystone_user['username'] return self.action.task.keystone_user["username"]
else: else:
id_manager = user_store.IdentityManager() 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 email = user.email
if email: if email:
return email return email
@ -316,17 +309,20 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
quota_config = CONF.quota.sizes.get(quota_size, {}) quota_config = CONF.quota.sizes.get(quota_size, {})
if not quota_config: if not quota_config:
self.add_note( self.add_note(
"Project quota not defined for size '%s' in region %s." % ( "Project quota not defined for size '%s' in region %s."
quota_size, region_name)) % (quota_size, region_name)
)
return return
quota_manager = QuotaManager( 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) quota_manager.set_region_quota(region_name, quota_config)
self.add_note("Project quota for region %s set to %s" % ( self.add_note(
region_name, quota_size)) "Project quota for region %s set to %s" % (region_name, quota_size)
)
def _can_auto_approve(self): def _can_auto_approve(self):
wait_days = self.config.days_between_autoapprove wait_days = self.config.days_between_autoapprove
@ -334,30 +330,34 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
completed_on__gte=timezone.now() - timedelta(days=wait_days), completed_on__gte=timezone.now() - timedelta(days=wait_days),
task_type__exact=self.action.task.task_type, task_type__exact=self.action.task.task_type,
cancelled__exact=False, cancelled__exact=False,
project_id__exact=self.project_id) project_id__exact=self.project_id,
)
changed_in_period = False changed_in_period = False
# Check to see if there have been any updates in the relavent regions # Check to see if there have been any updates in the relavent regions
# recently # recently
for task in task_list: for task in task_list:
for action in task.actions: for action in task.actions:
intersect = set(action.action_data[ intersect = set(action.action_data["regions"]).intersection(
'regions']).intersection(self.regions) self.regions
)
if intersect: if intersect:
changed_in_period = True changed_in_period = True
region_sizes = [] region_sizes = []
quota_manager = QuotaManager( quota_manager = QuotaManager(
self.project_id, self.config.size_difference_threshold) self.project_id, self.config.size_difference_threshold
)
for region in self.regions: for region in self.regions:
current_size = quota_manager.get_region_quota_data( 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) region_sizes.append(current_size)
self.add_note( self.add_note(
"Project has size '%s' in region: '%s'" % "Project has size '%s' in region: '%s'" % (current_size, region)
(current_size, region)) )
# Check for preapproved_quotas # Check for preapproved_quotas
preapproved_quotas = [] preapproved_quotas = []
@ -365,42 +365,44 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
# If all region sizes are the same # If all region sizes are the same
if region_sizes.count(region_sizes[0]) == len(region_sizes): if region_sizes.count(region_sizes[0]) == len(region_sizes):
preapproved_quotas = quota_manager.get_quota_change_options( preapproved_quotas = quota_manager.get_quota_change_options(region_sizes[0])
region_sizes[0]) smaller_quotas = quota_manager.get_smaller_quota_options(region_sizes[0])
smaller_quotas = quota_manager.get_smaller_quota_options(
region_sizes[0])
if self.size in smaller_quotas: if self.size in smaller_quotas:
self.add_note( self.add_note(
"Quota size '%s' is in list of smaller quotas: %s" % "Quota size '%s' is in list of smaller quotas: %s"
(self.size, smaller_quotas)) % (self.size, smaller_quotas)
)
return True return True
if changed_in_period: if changed_in_period:
self.add_note( self.add_note(
"Quota has already been updated within the auto " "Quota has already been updated within the auto " "approve time limit."
"approve time limit.") )
return False return False
if self.size not in preapproved_quotas: if self.size not in preapproved_quotas:
self.add_note( self.add_note(
"Quota size '%s' not in preapproved list: %s" % "Quota size '%s' not in preapproved list: %s"
(self.size, preapproved_quotas)) % (self.size, preapproved_quotas)
)
return False return False
self.add_note( self.add_note(
"Quota size '%s' in preapproved list: %s" % "Quota size '%s' in preapproved list: %s" % (self.size, preapproved_quotas)
(self.size, preapproved_quotas)) )
return True return True
def _validate(self): def _validate(self):
# Make sure the project id is valid and can be used # Make sure the project id is valid and can be used
self.action.valid = validate_steps([ self.action.valid = validate_steps(
self._validate_project_id, [
self._validate_quota_size_exists, self._validate_project_id,
self._validate_regions_exist, self._validate_quota_size_exists,
self._validate_usage_lower_than_quota, self._validate_regions_exist,
]) self._validate_usage_lower_than_quota,
]
)
self.action.save() self.action.save()
def _prepare(self): def _prepare(self):
@ -420,8 +422,8 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
self._set_region_quota(region, self.size) self._set_region_quota(region, self.size)
self.action.state = "completed" self.action.state = "completed"
self.action.task.cache['project_id'] = self.project_id self.action.task.cache["project_id"] = self.project_id
self.action.task.cache['size'] = self.size self.action.task.cache["size"] = self.size
self.action.save() self.action.save()
@ -434,6 +436,7 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
class SetProjectQuotaAction(UpdateProjectQuotasAction): class SetProjectQuotaAction(UpdateProjectQuotasAction):
""" Updates quota for a given project to a configured quota level """ """ Updates quota for a given project to a configured quota level """
required = [] required = []
serializer = serializers.SetProjectQuotaSerializer serializer = serializers.SetProjectQuotaSerializer
@ -454,9 +457,7 @@ class SetProjectQuotaAction(UpdateProjectQuotasAction):
def _validate(self): def _validate(self):
# Make sure the project id is valid and can be used # Make sure the project id is valid and can be used
self.action.valid = validate_steps([ self.action.valid = validate_steps([self._validate_project_id,])
self._validate_project_id,
])
self.action.save() self.action.save()
def _prepare(self): def _prepare(self):
@ -466,7 +467,7 @@ class SetProjectQuotaAction(UpdateProjectQuotasAction):
def _approve(self): def _approve(self):
# Assumption: another action has placed the project_id into the cache. # 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() self._validate()
if not self.valid or self.action.state == "completed": 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. 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) username = serializers.CharField(max_length=255)
email = serializers.EmailField() email = serializers.EmailField()
@ -40,7 +41,7 @@ class BaseUserNameSerializer(serializers.Serializer):
super(BaseUserNameSerializer, self).__init__(*args, **kwargs) super(BaseUserNameSerializer, self).__init__(*args, **kwargs)
if CONF.identity.username_is_email: if CONF.identity.username_is_email:
self.fields.pop('username') self.fields.pop("username")
class BaseUserIdSerializer(serializers.Serializer): class BaseUserIdSerializer(serializers.Serializer):
@ -53,35 +54,36 @@ class NewUserSerializer(BaseUserNameSerializer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(NewUserSerializer, self).__init__(*args, **kwargs) super(NewUserSerializer, self).__init__(*args, **kwargs)
# NOTE(adriant): This overide is mostly in use so that it can be tested # NOTE(adriant): This overide is mostly in use so that it can be tested
self.fields['roles'] = serializers.MultipleChoiceField( self.fields["roles"] = serializers.MultipleChoiceField(
choices=get_role_choices(), default=set) choices=get_role_choices(), default=set
self.fields['inherited_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): 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( raise serializers.ValidationError(
"Must supply either 'roles' or 'inherited_roles', or both.") "Must supply either 'roles' or 'inherited_roles', or both."
)
return data return data
class NewProjectSerializer(serializers.Serializer): class NewProjectSerializer(serializers.Serializer):
parent_id = serializers.CharField( parent_id = serializers.CharField(max_length=64, default=None, allow_null=True)
max_length=64, default=None, allow_null=True)
project_name = serializers.CharField(max_length=64) 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) description = serializers.CharField(default="", allow_blank=True)
class NewProjectWithUserSerializer(BaseUserNameSerializer): class NewProjectWithUserSerializer(BaseUserNameSerializer):
parent_id = serializers.CharField( parent_id = serializers.CharField(max_length=64, default=None, allow_null=True)
max_length=64, default=None, allow_null=True)
project_name = serializers.CharField(max_length=64) project_name = serializers.CharField(max_length=64)
class ResetUserPasswordSerializer(BaseUserNameSerializer): 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. # override domain_id so serializer doesn't set it up.
domain_id = None domain_id = None
@ -93,15 +95,18 @@ class EditUserRolesSerializer(BaseUserIdSerializer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditUserRolesSerializer, self).__init__(*args, **kwargs) super(EditUserRolesSerializer, self).__init__(*args, **kwargs)
# NOTE(adriant): This overide is mostly in use so that it can be tested # NOTE(adriant): This overide is mostly in use so that it can be tested
self.fields['roles'] = serializers.MultipleChoiceField( self.fields["roles"] = serializers.MultipleChoiceField(
choices=get_role_choices(), default=set) choices=get_role_choices(), default=set
self.fields['inherited_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): 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( raise serializers.ValidationError(
"Must supply either 'roles' or 'inherited_roles', or both.") "Must supply either 'roles' or 'inherited_roles', or both."
)
return data return data
@ -118,7 +123,7 @@ class NewProjectDefaultNetworkSerializer(serializers.Serializer):
class AddDefaultUsersToProjectSerializer(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): 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 # 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 # However it does take into account the improbable edge case that the
# regions have changed since the server was last started # regions have changed since the server was last started
self.fields['regions'] = serializers.MultipleChoiceField( self.fields["regions"] = serializers.MultipleChoiceField(
choices=get_region_choices()) choices=get_region_choices()
)
def validate_size(self, value): def validate_size(self, value):
""" """
@ -151,6 +157,5 @@ class UpdateProjectQuotasSerializer(serializers.Serializer):
""" """
size_list = CONF.quota.sizes.keys() size_list = CONF.quota.sizes.keys()
if value not in size_list: if value not in size_list:
raise serializers.ValidationError("Quota size: %s is not valid" raise serializers.ValidationError("Quota size: %s is not valid" % value)
% value)
return value return value

View File

@ -27,11 +27,11 @@ from adjutant.common.tests.utils import AdjutantTestCase
from adjutant.config import CONF from adjutant.config import CONF
default_email_conf = { default_email_conf = {
'from': "adjutant@example.com", "from": "adjutant@example.com",
'reply': 'adjutant@example.com', "reply": "adjutant@example.com",
'template': 'initial.txt', "template": "initial.txt",
'html_template': 'completed.txt', "html_template": "completed.txt",
'subject': 'additional email' "subject": "additional email",
} }
@ -40,22 +40,15 @@ class FailEmail(mock.MagicMock):
raise SMTPException raise SMTPException
@mock.patch('adjutant.common.user_store.IdentityManager', @mock.patch("adjutant.common.user_store.IdentityManager", FakeManager)
FakeManager)
class MiscActionTests(AdjutantTestCase): class MiscActionTests(AdjutantTestCase):
def test_send_email(self): def test_send_email(self):
# include html template # include html template
to_address = "test@example.com" to_address = "test@example.com"
task = Task.objects.create( task = Task.objects.create(keystone_user={})
keystone_user={}
)
context = { context = {"task": task, "actions": ["action_1", "action_2"]}
'task': task,
'actions': ["action_1", "action_2"]
}
result = send_email(to_address, context, default_email_conf, task) result = send_email(to_address, context, default_email_conf, task)
@ -67,58 +60,53 @@ class MiscActionTests(AdjutantTestCase):
def test_send_email_no_addresses(self): def test_send_email_no_addresses(self):
to_address = [] to_address = []
task = Task.objects.create( task = Task.objects.create(keystone_user={})
keystone_user={}
)
context = { context = {"task": task, "actions": ["action_1", "action_2"]}
'task': task,
'actions': ["action_1", "action_2"]
}
result = send_email(to_address, context, default_email_conf, task) result = send_email(to_address, context, default_email_conf, task)
self.assertEqual(result, None) self.assertEqual(result, None)
self.assertEqual(len(mail.outbox), 0) self.assertEqual(len(mail.outbox), 0)
@mock.patch('adjutant.actions.utils.EmailMultiAlternatives', @mock.patch("adjutant.actions.utils.EmailMultiAlternatives", FailEmail)
FailEmail)
@conf_utils.modify_conf( @conf_utils.modify_conf(
CONF, CONF,
operations={ operations={
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [ "adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
{'operation': 'overlay', 'value': { {
'email_task_cache': True, "operation": "overlay",
'subject': 'Email Subject', "value": {
'template': 'token.txt' "email_task_cache": True,
}}, "subject": "Email Subject",
"template": "token.txt",
},
},
], ],
}) },
)
def test_send_additional_email_fail(self): def test_send_additional_email_fail(self):
""" """
Tests that a failure to send an additional email doesn't cause Tests that a failure to send an additional email doesn't cause
it to become invalid or break. it to become invalid or break.
""" """
task = Task.objects.create( task = Task.objects.create(keystone_user={}, task_type="edit_roles",)
keystone_user={},
task_type='edit_roles',
)
action = SendAdditionalEmailAction({}, task=task, order=1) action = SendAdditionalEmailAction({}, task=task, order=1)
action.prepare() action.prepare()
self.assertEqual(action.valid, True) self.assertEqual(action.valid, True)
task.cache["additional_emails"] = ["thisguy@righthere.com", task.cache["additional_emails"] = ["thisguy@righthere.com", "nope@example.com"]
"nope@example.com"]
action.approve() action.approve()
self.assertEqual(action.valid, True) self.assertEqual(action.valid, True)
self.assertEqual(len(mail.outbox), 0) self.assertEqual(len(mail.outbox), 0)
self.assertTrue( self.assertTrue(
"Unable to send additional email. Stage: approve" in "Unable to send additional email. Stage: approve"
action.action.task.action_notes['SendAdditionalEmailAction'][1]) in action.action.task.action_notes["SendAdditionalEmailAction"][1]
)
action.submit({}) action.submit({})
self.assertEqual(action.valid, True) self.assertEqual(action.valid, True)
@ -127,37 +115,39 @@ class MiscActionTests(AdjutantTestCase):
CONF, CONF,
operations={ operations={
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [ "adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
{'operation': 'overlay', 'value': { {
'email_task_cache': True, "operation": "overlay",
'subject': 'Email Subject', "value": {
'template': 'token.txt' "email_task_cache": True,
}}, "subject": "Email Subject",
"template": "token.txt",
},
},
], ],
}) },
)
def test_send_additional_email_task_cache(self): def test_send_additional_email_task_cache(self):
""" """
Tests sending an additional email with the address placed in the Tests sending an additional email with the address placed in the
task cache. task cache.
""" """
task = Task.objects.create( task = Task.objects.create(keystone_user={})
keystone_user={}
)
action = SendAdditionalEmailAction({}, task=task, order=1) action = SendAdditionalEmailAction({}, task=task, order=1)
action.prepare() action.prepare()
self.assertEqual(action.valid, True) self.assertEqual(action.valid, True)
task.cache["additional_emails"] = ["thisguy@righthere.com", task.cache["additional_emails"] = ["thisguy@righthere.com", "nope@example.com"]
"nope@example.com"]
action.approve() action.approve()
self.assertEqual(action.valid, True) self.assertEqual(action.valid, True)
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
self.assertEqual(set(mail.outbox[0].to), self.assertEqual(
set(["thisguy@righthere.com", "nope@example.com"])) set(mail.outbox[0].to), set(["thisguy@righthere.com", "nope@example.com"])
)
action.submit({}) action.submit({})
self.assertEqual(action.valid, True) self.assertEqual(action.valid, True)
@ -167,22 +157,24 @@ class MiscActionTests(AdjutantTestCase):
CONF, CONF,
operations={ operations={
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [ "adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
{'operation': 'overlay', 'value': { {
'email_task_cache': True, "operation": "overlay",
'subject': 'Email Subject', "value": {
'template': 'token.txt' "email_task_cache": True,
}}, "subject": "Email Subject",
"template": "token.txt",
},
},
], ],
}) },
)
def test_send_additional_email_task_cache_none_set(self): def test_send_additional_email_task_cache_none_set(self):
""" """
Tests sending an additional email with 'email_task_cache' set but Tests sending an additional email with 'email_task_cache' set but
no address placed in the task cache. no address placed in the task cache.
""" """
task = Task.objects.create( task = Task.objects.create(keystone_user={})
keystone_user={}
)
action = SendAdditionalEmailAction({}, task=task, order=1) action = SendAdditionalEmailAction({}, task=task, order=1)
@ -201,23 +193,24 @@ class MiscActionTests(AdjutantTestCase):
CONF, CONF,
operations={ operations={
"adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [ "adjutant.workflow.action_defaults.SendAdditionalEmailAction.approve": [
{'operation': 'overlay', 'value': { {
'email_additional_addresses': [ "operation": "overlay",
'anadminwhocares@example.com'], "value": {
'subject': 'Email Subject', "email_additional_addresses": ["anadminwhocares@example.com"],
'template': 'token.txt' "subject": "Email Subject",
}}, "template": "token.txt",
},
},
], ],
}) },
)
def test_send_additional_email_email_in_config(self): def test_send_additional_email_email_in_config(self):
""" """
Tests sending an additional email with the address placed in the Tests sending an additional email with the address placed in the
task cache. task cache.
""" """
task = Task.objects.create( task = Task.objects.create(keystone_user={})
keystone_user={}
)
action = SendAdditionalEmailAction({}, task=task, order=1) action = SendAdditionalEmailAction({}, task=task, order=1)
@ -228,8 +221,7 @@ class MiscActionTests(AdjutantTestCase):
self.assertEqual(action.valid, True) self.assertEqual(action.valid, True)
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, self.assertEqual(mail.outbox[0].to, ["anadminwhocares@example.com"])
["anadminwhocares@example.com"])
action.submit({}) action.submit({})
self.assertEqual(action.valid, True) 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.config import CONF
from adjutant.common import user_store from adjutant.common import user_store
from adjutant.actions.v1.base import ( from adjutant.actions.v1.base import (
UserNameAction, UserIdAction, UserMixin, ProjectMixin) UserNameAction,
UserIdAction,
UserMixin,
ProjectMixin,
)
from adjutant.actions.v1 import serializers from adjutant.actions.v1 import serializers
from adjutant.actions.utils import validate_steps from adjutant.actions.utils import validate_steps
@ -32,12 +36,12 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
""" """
required = [ required = [
'username', "username",
'email', "email",
'project_id', "project_id",
'roles', "roles",
'inherited_roles', "inherited_roles",
'domain_id', "domain_id",
] ]
serializer = serializers.NewUserSerializer serializer = serializers.NewUserSerializer
@ -51,37 +55,43 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
if not user: if not user:
self.add_note( self.add_note(
"No user present with username '%s'. " "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: if not id_manager.can_edit_users:
self.add_note( self.add_note(
'Identity backend does not support user editing, ' "Identity backend does not support user editing, "
'cannot create new user.') "cannot create new user."
)
return False return False
self.action.need_token = True self.action.need_token = True
# add to cache to use in template # 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"]) self.set_token_fields(["password"])
return True return True
if (not CONF.identity.username_is_email if (
and getattr(user, 'email', None) != self.email): not CONF.identity.username_is_email
and getattr(user, "email", None) != self.email
):
self.add_note( self.add_note(
'Found matching username, but email did not match. ' "Found matching username, but email did not match. "
'Reporting as invalid.') "Reporting as invalid."
)
return False return False
if not user.enabled: if not user.enabled:
self.add_note( self.add_note(
"Existing disabled user '%s' with matching email." % "Existing disabled user '%s' with matching email." % self.email
self.email) )
if not id_manager.can_edit_users: if not id_manager.can_edit_users:
self.add_note( self.add_note(
'Identity backend does not support user editing, ' "Identity backend does not support user editing, "
'cannot renable user.') "cannot renable user."
)
return False return False
self.action.need_token = True self.action.need_token = True
self.action.state = "disabled" self.action.state = "disabled"
# add to cache to use in template # 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 # as they are disabled we'll reset their password
self.set_token_fields(["password"]) self.set_token_fields(["password"])
return True return True
@ -93,30 +103,29 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
if not missing: if not missing:
self.action.need_token = False self.action.need_token = False
self.action.state = "complete" self.action.state = "complete"
self.add_note( self.add_note("Existing user already has roles.")
'Existing user already has roles.'
)
else: else:
self.roles = list(missing) self.roles = list(missing)
self.action.need_token = True self.action.need_token = True
self.set_token_fields(["confirm"]) self.set_token_fields(["confirm"])
self.action.state = "existing" self.action.state = "existing"
# add to cache to use in template # add to cache to use in template
self.action.task.cache['user_state'] = "existing" self.action.task.cache["user_state"] = "existing"
self.add_note( self.add_note("Existing user with matching email missing roles.")
'Existing user with matching email missing roles.')
return True return True
def _validate(self): def _validate(self):
self.action.valid = validate_steps([ self.action.valid = validate_steps(
self._validate_role_permissions, [
self._validate_keystone_user_domain_id, self._validate_role_permissions,
self._validate_keystone_user_project_id, self._validate_keystone_user_domain_id,
self._validate_domain_id, self._validate_keystone_user_project_id,
self._validate_project_id, self._validate_domain_id,
self._validate_target_user, self._validate_project_id,
]) self._validate_target_user,
]
)
self.action.save() self.action.save()
def _prepare(self): def _prepare(self):
@ -134,13 +143,14 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
if self.action.state == "default": if self.action.state == "default":
# default action: Create a new user in the tenant and add roles # 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.roles, self.project_id)
self.grant_roles(user, self.inherited_roles, self.project_id, True) self.grant_roles(user, self.inherited_roles, self.project_id, True)
self.add_note( self.add_note(
'User %s has been created, with roles %s in project %s.' "User %s has been created, with roles %s in project %s."
% (self.username, self.roles, self.project_id)) % (self.username, self.roles, self.project_id)
)
elif self.action.state == "disabled": elif self.action.state == "disabled":
# first re-enable user # first re-enable user
@ -148,14 +158,14 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
self.enable_user(user) self.enable_user(user)
self.grant_roles(user, self.roles, self.project_id) self.grant_roles(user, self.roles, self.project_id)
self.grant_roles(user, self.inherited_roles, self.project_id, True) 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( self.add_note(
'Existing user %s has been re-enabled and given roles %s' "Existing user %s has been re-enabled and given roles %s"
' in project %s.' " in project %s." % (self.username, self.roles, self.project_id)
% (self.username, self.roles, self.project_id)) )
elif self.action.state == "existing": elif self.action.state == "existing":
# Existing action: only add roles. # 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.grant_roles(user, self.inherited_roles, self.project_id, True)
self.add_note( self.add_note(
'Existing user %s has been given roles %s in project %s.' "Existing user %s has been given roles %s in project %s."
% (self.username, self.roles, self.project_id)) % (self.username, self.roles, self.project_id)
)
elif self.action.state == "complete": elif self.action.state == "complete":
# complete action: nothing to do. # complete action: nothing to do.
self.add_note( self.add_note(
'Existing user %s already had roles %s in project %s.' "Existing user %s already had roles %s in project %s."
% (self.username, self.roles, self.project_id)) % (self.username, self.roles, self.project_id)
)
class ResetUserPasswordAction(UserNameAction, UserMixin): class ResetUserPasswordAction(UserNameAction, UserMixin):
@ -178,11 +190,7 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
Simple action to reset a password for a given user. Simple action to reset a password for a given user.
""" """
required = [ required = ["domain_name", "username", "email"]
'domain_name',
'username',
'email'
]
serializer = serializers.ResetUserPasswordSerializer serializer = serializers.ResetUserPasswordSerializer
@ -192,7 +200,7 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
"blacklisted_roles", "blacklisted_roles",
help_text="Users with these roles cannot reset their passwords.", help_text="Users with these roles cannot reset their passwords.",
default=[], default=[],
sample_default=['admin'], sample_default=["admin"],
), ),
], ],
) )
@ -210,7 +218,7 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
user_roles.extend(role.name for role in roles) user_roles.extend(role.name for role in roles)
if set(self.config.blacklisted_roles) & set(user_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 False
return True return True
@ -219,26 +227,28 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
# NOTE(adriant): We only need to check the USERNAME_IS_EMAIL=False # NOTE(adriant): We only need to check the USERNAME_IS_EMAIL=False
# case since '_validate_username_exists' will ensure the True case # case since '_validate_username_exists' will ensure the True case
if not CONF.identity.username_is_email: if not CONF.identity.username_is_email:
if (self.user and ( if self.user and (
getattr(self.user, 'email', None).lower() getattr(self.user, "email", None).lower() != self.email.lower()
!= self.email.lower())): ):
self.add_note('Existing user with non-matching email.') self.add_note("Existing user with non-matching email.")
return False return False
self.action.need_token = True self.action.need_token = True
self.set_token_fields(["password"]) self.set_token_fields(["password"])
self.add_note('Existing user with matching email.') self.add_note("Existing user with matching email.")
return True return True
def _validate(self): def _validate(self):
# Here, the order of validation matters # Here, the order of validation matters
# as each one adds new class variables # as each one adds new class variables
self.action.valid = validate_steps([ self.action.valid = validate_steps(
self._validate_domain_name, [
self._validate_username_exists, self._validate_domain_name,
self._validate_user_roles, self._validate_username_exists,
self._validate_user_email, self._validate_user_roles,
]) self._validate_user_email,
]
)
self.action.save() self.action.save()
def _prepare(self): def _prepare(self):
@ -254,8 +264,8 @@ class ResetUserPasswordAction(UserNameAction, UserMixin):
if not self.valid: if not self.valid:
return return
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)
class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin): class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
@ -264,13 +274,7 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
on a user for the given project. on a user for the given project.
""" """
required = [ required = ["project_id", "user_id", "roles", "inherited_roles", "remove"]
'project_id',
'user_id',
'roles',
'inherited_roles',
'remove'
]
serializer = serializers.EditUserRolesSerializer serializer = serializers.EditUserRolesSerializer
@ -278,7 +282,7 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
# Get target user # Get target user
user = self._get_target_user() user = self._get_target_user()
if not 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 False
return True return True
@ -288,37 +292,31 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
project = id_manager.get_project(self.project_id) project = id_manager.get_project(self.project_id)
# user roles # user roles
current_roles = id_manager.get_roles(user, project) current_roles = id_manager.get_roles(user, project)
current_inherited_roles = id_manager.get_roles( current_inherited_roles = id_manager.get_roles(user, project, inherited=True)
user, project, inherited=True)
current_roles = {role.name for role in current_roles} current_roles = {role.name for role in current_roles}
current_inherited_roles = { current_inherited_roles = {role.name for role in current_inherited_roles}
role.name for role in current_inherited_roles}
if self.remove: if self.remove:
remaining = set(current_roles) & set(self.roles) remaining = set(current_roles) & set(self.roles)
remaining_inherited = ( remaining_inherited = set(current_inherited_roles) & set(
set(current_inherited_roles) & set(self.inherited_roles)) self.inherited_roles
)
if not remaining and not remaining_inherited: if not remaining and not remaining_inherited:
self.action.state = "complete" self.action.state = "complete"
self.add_note( self.add_note("User doesn't have roles to remove.")
"User doesn't have roles to remove.")
else: else:
self.roles = list(remaining) self.roles = list(remaining)
self.inherited_roles = list(remaining_inherited) self.inherited_roles = list(remaining_inherited)
self.add_note( self.add_note("User has roles to remove.")
'User has roles to remove.')
else: else:
missing = set(self.roles) - set(current_roles) missing = set(self.roles) - set(current_roles)
missing_inherited = ( missing_inherited = set(self.inherited_roles) - set(current_inherited_roles)
set(self.inherited_roles) - set(current_inherited_roles))
if not missing and not missing_inherited: if not missing and not missing_inherited:
self.action.state = "complete" self.action.state = "complete"
self.add_note( self.add_note("User already has roles.")
'User already has roles.')
else: else:
self.roles = list(missing) self.roles = list(missing)
self.inherited_roles = list(missing_inherited) self.inherited_roles = list(missing_inherited)
self.add_note( self.add_note("User missing roles.")
'User missing roles.')
# All paths are valid here # All paths are valid here
# We've just set state and roles that need to be changed. # We've just set state and roles that need to be changed.
return True return True
@ -327,18 +325,21 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
current_user_roles = id_manager.get_roles(project=self.project_id, current_user_roles = id_manager.get_roles(
user=self.user_id) project=self.project_id, user=self.user_id
)
current_user_roles = [role.name for role in current_user_roles] current_user_roles = [role.name for role in current_user_roles]
current_roles_manageable = self.are_roles_manageable( 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 = set()
all_roles.update(self.roles) all_roles.update(self.roles)
all_roles.update(self.inherited_roles) all_roles.update(self.inherited_roles)
new_roles_manageable = self.are_roles_manageable( 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: if new_roles_manageable and current_roles_manageable:
self.add_note("All user roles are manageable.") self.add_note("All user roles are manageable.")
@ -347,13 +348,15 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
return False return False
def _validate(self): def _validate(self):
self.action.valid = validate_steps([ self.action.valid = validate_steps(
self._validate_keystone_user_project_id, [
self._validate_role_permissions, self._validate_keystone_user_project_id,
self._validate_project_id, self._validate_role_permissions,
self._validate_target_user, self._validate_project_id,
self._validate_user_roles, self._validate_target_user,
]) self._validate_user_roles,
]
)
self.action.save() self.action.save()
def _prepare(self): def _prepare(self):
@ -371,37 +374,47 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
if self.action.state == "default": if self.action.state == "default":
user = self._get_target_user() user = self._get_target_user()
self._user_roles_edit(user, self.roles, self.project_id, self._user_roles_edit(user, self.roles, self.project_id, remove=self.remove)
remove=self.remove) self._user_roles_edit(
self._user_roles_edit(user, self.inherited_roles, self.project_id, user,
remove=self.remove, inherited=True) self.inherited_roles,
self.project_id,
remove=self.remove,
inherited=True,
)
if self.remove and self.roles: if self.remove and self.roles:
self.add_note( self.add_note(
'User %s has had roles %s removed from project %s.' "User %s has had roles %s removed from project %s."
% (self.user_id, self.roles, self.project_id)) % (self.user_id, self.roles, self.project_id)
)
if self.remove and self.inherited_roles: if self.remove and self.inherited_roles:
self.add_note( self.add_note(
'User %s has had inherited roles %s ' "User %s has had inherited roles %s "
'removed from project %s.' "removed from project %s."
% (self.user_id, self.inherited_roles, self.project_id)) % (self.user_id, self.inherited_roles, self.project_id)
)
if self.roles: if self.roles:
self.add_note( self.add_note(
'User %s has been given roles %s in project %s.' "User %s has been given roles %s in project %s."
% (self.user_id, self.roles, self.project_id)) % (self.user_id, self.roles, self.project_id)
)
if self.inherited_roles: if self.inherited_roles:
self.add_note( self.add_note(
'User %s has been given inherited roles %s in project %s.' "User %s has been given inherited roles %s in project %s."
% (self.user_id, self.inherited_roles, self.project_id)) % (self.user_id, self.inherited_roles, self.project_id)
)
elif self.action.state == "complete": elif self.action.state == "complete":
if self.remove: if self.remove:
self.add_note( self.add_note(
"User %s didn't have roles %s in project %s." "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: else:
self.add_note( self.add_note(
'User %s already had roles %s in project %s.' "User %s already had roles %s in project %s."
% (self.user_id, self.roles, self.project_id)) % (self.user_id, self.roles, self.project_id)
)
class UpdateUserEmailAction(UserIdAction, UserMixin): class UpdateUserEmailAction(UserIdAction, UserMixin):
@ -410,8 +423,8 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
""" """
required = [ required = [
'user_id', "user_id",
'new_email', "new_email",
] ]
serializer = serializers.UpdateUserEmailSerializer serializer = serializers.UpdateUserEmailSerializer
@ -421,10 +434,9 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
return self.new_email return self.new_email
def _validate(self): def _validate(self):
self.action.valid = validate_steps([ self.action.valid = validate_steps(
self._validate_user, [self._validate_user, self._validate_email_not_in_use,]
self._validate_email_not_in_use, )
])
self.action.save() self.action.save()
def _validate_user(self): def _validate_user(self):
@ -436,8 +448,7 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
def _validate_email_not_in_use(self): def _validate_email_not_in_use(self):
if CONF.identity.username_is_email: if CONF.identity.username_is_email:
self.domain_id = self.action.task.keystone_user[ self.domain_id = self.action.task.keystone_user["project_domain_id"]
'project_domain_id']
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
@ -469,5 +480,7 @@ class UpdateUserEmailAction(UserIdAction, UserMixin):
if CONF.identity.username_is_email: if CONF.identity.username_is_email:
self.update_user_name(self.new_email, user=self.user) self.update_user_name(self.new_email, user=self.user)
self.add_note('The email for user %s has been changed to %s.' self.add_note(
% (self.old_username, self.new_email)) "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 from adjutant.notifications.utils import create_notification
LOG = getLogger('adjutant') LOG = getLogger("adjutant")
def exception_handler(exc, context): def exception_handler(exc, context):
@ -38,17 +38,17 @@ def exception_handler(exc, context):
if isinstance(exc, exceptions.BaseAPIException): if isinstance(exc, exceptions.BaseAPIException):
if isinstance(exc.message, (list, dict)): if isinstance(exc.message, (list, dict)):
data = {'errors': exc.message} data = {"errors": exc.message}
else: else:
data = {'errors': [exc.message]} data = {"errors": [exc.message]}
note_data = data note_data = data
if isinstance(exc, exceptions.TaskActionsFailed): if isinstance(exc, exceptions.TaskActionsFailed):
if exc.internal_message: if exc.internal_message:
if isinstance(exc.internal_message, (list, dict)): if isinstance(exc.internal_message, (list, dict)):
note_data = {'errors': exc.internal_message} note_data = {"errors": exc.internal_message}
else: else:
note_data = {'errors': [exc.internal_message]} note_data = {"errors": [exc.internal_message]}
create_notification(exc.task, note_data, error=True) create_notification(exc.task, note_data, error=True)
LOG.info("(%s) - %s" % (now, exc)) LOG.info("(%s) - %s" % (now, exc))

View File

@ -9,50 +9,78 @@ import adjutant.api.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Notification', name="Notification",
fields=[ fields=[
('uuid', models.CharField(default=adjutant.api.models.hex_uuid, max_length=32, serialize=False, primary_key=True)), (
('notes', jsonfield.fields.JSONField(default={})), "uuid",
('error', models.BooleanField(default=False, db_index=True)), models.CharField(
('created_on', models.DateTimeField(default=django.utils.timezone.now)), default=adjutant.api.models.hex_uuid,
('acknowledged', models.BooleanField(default=False, db_index=True)), 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( migrations.CreateModel(
name='Task', name="Task",
fields=[ 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)), "uuid",
('ip_address', models.GenericIPAddressField()), models.CharField(
('keystone_user', jsonfield.fields.JSONField(default={})), default=adjutant.api.models.hex_uuid,
('project_id', models.CharField(max_length=32, null=True, db_index=True)), max_length=32,
('task_type', models.CharField(max_length=100, db_index=True)), serialize=False,
('action_notes', jsonfield.fields.JSONField(default={})), primary_key=True,
('cancelled', models.BooleanField(default=False, db_index=True)), ),
('approved', models.BooleanField(default=False, db_index=True)), ),
('completed', models.BooleanField(default=False, db_index=True)), ("hash_key", models.CharField(max_length=32, db_index=True)),
('created_on', models.DateTimeField(default=django.utils.timezone.now)), ("ip_address", models.GenericIPAddressField()),
('approved_on', models.DateTimeField(null=True)), ("keystone_user", jsonfield.fields.JSONField(default={})),
('completed_on', models.DateTimeField(null=True)), (
"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( migrations.CreateModel(
name='Token', name="Token",
fields=[ fields=[
('token', models.CharField(max_length=32, serialize=False, primary_key=True)), (
('created_on', models.DateTimeField(default=django.utils.timezone.now)), "token",
('expires', models.DateTimeField(db_index=True)), models.CharField(max_length=32, serialize=False, primary_key=True),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Task')), ),
("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( migrations.AddField(
model_name='notification', model_name="notification",
name='task', name="task",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('api', '0001_initial'), ("api", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='task', model_name="task",
name='hash_key', name="hash_key",
field=models.CharField(max_length=64, db_index=True), field=models.CharField(max_length=64, db_index=True),
), ),
] ]

View File

@ -8,13 +8,13 @@ import jsonfield.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('api', '0002_auto_20160815_2249'), ("api", "0002_auto_20160815_2249"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='task', model_name="task",
name='approved_by', name="approved_by",
field=jsonfield.fields.JSONField(default={}), field=jsonfield.fields.JSONField(default={}),
), ),
] ]

View File

@ -7,13 +7,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('api', '0003_task_approved_by'), ("api", "0003_task_approved_by"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='task', model_name="task",
name='project_id', name="project_id",
field=models.CharField(max_length=64, null=True, db_index=True), field=models.CharField(max_length=64, null=True, db_index=True),
), ),
] ]

View File

@ -10,16 +10,13 @@ class Migration(migrations.Migration):
atomic = False atomic = False
dependencies = [ dependencies = [
('api', '0004_auto_20160929_0317'), ("api", "0004_auto_20160929_0317"),
] ]
operations = [ operations = [
migrations.SeparateDatabaseAndState( migrations.SeparateDatabaseAndState(
database_operations=[ database_operations=[
migrations.AlterModelTable( migrations.AlterModelTable(name="task", table="tasks_task",),
name='task',
table='tasks_task',
),
], ],
), ),
] ]

View File

@ -9,14 +9,16 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('tasks', '0001_initial'), ("tasks", "0001_initial"),
('actions', '0003_auto_20190610_0205'), ("actions", "0003_auto_20190610_0205"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='token', model_name="token",
name='task', name="task",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('tasks', '0001_initial'), ("tasks", "0001_initial"),
('actions', '0003_auto_20190610_0205'), ("actions", "0003_auto_20190610_0205"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='notification', model_name="notification",
name='task', name="task",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('api', '0005_auto_20190610_0209'), ("api", "0005_auto_20190610_0209"),
('tasks', '0001_initial'), ("tasks", "0001_initial"),
('actions', '0004_auto_20190610_0209'), ("actions", "0004_auto_20190610_0209"),
('api', '0006_auto_20190610_0209'), ("api", "0006_auto_20190610_0209"),
('api', '0007_auto_20190610_0209'), ("api", "0007_auto_20190610_0209"),
] ]
operations = [ operations = [
migrations.SeparateDatabaseAndState( migrations.SeparateDatabaseAndState(
state_operations=[ state_operations=[migrations.DeleteModel(name="Task",),],
migrations.DeleteModel(
name='Task',
),
],
), ),
] ]

View File

@ -40,7 +40,7 @@ class Token(models.Model):
"task_type": self.task.task_type, "task_type": self.task.task_type,
"token": self.token, "token": self.token,
"created_on": self.created_on, "created_on": self.created_on,
"expires": self.expires "expires": self.expires,
} }
@property @property
@ -53,8 +53,7 @@ class Notification(models.Model):
Notification linked to a task with some notes. Notification linked to a task with some notes.
""" """
uuid = models.CharField(max_length=32, default=hex_uuid, uuid = models.CharField(max_length=32, default=hex_uuid, primary_key=True)
primary_key=True)
notes = JSONField(default={}) notes = JSONField(default={})
task = models.ForeignKey(Task, on_delete=models.CASCADE) task = models.ForeignKey(Task, on_delete=models.CASCADE)
error = models.BooleanField(default=False, db_index=True) error = models.BooleanField(default=False, db_index=True)
@ -68,5 +67,5 @@ class Notification(models.Model):
"task": self.task.uuid, "task": self.task.uuid,
"error": self.error, "error": self.error,
"acknowledged": self.acknowledged, "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 from adjutant.api.v1 import views as views_v1
urlpatterns = [ urlpatterns = [
url(r'^$', views.VersionView.as_view()), url(r"^$", views.VersionView.as_view()),
] ]
# NOTE(adriant): make this conditional once we have a v2. # NOTE(adriant): make this conditional once we have a v2.
build_version_details('1.0', 'CURRENT', relative_endpoint='v1/') build_version_details("1.0", "CURRENT", relative_endpoint="v1/")
urlpatterns.append(url(r'^v1/?$', views_v1.V1VersionEndpoint.as_view())) urlpatterns.append(url(r"^v1/?$", views_v1.V1VersionEndpoint.as_view()))
urlpatterns.append(url(r'^v1/', include('adjutant.api.v1.urls'))) urlpatterns.append(url(r"^v1/", include("adjutant.api.v1.urls")))
if settings.DEBUG: if settings.DEBUG:
schema_view = get_swagger_view(title='Adjutant API') schema_view = get_swagger_view(title="Adjutant API")
urlpatterns.append(url(r'^docs/', schema_view)) urlpatterns.append(url(r"^docs/", schema_view))

View File

@ -27,17 +27,17 @@ def require_roles(roles, func, *args, **kwargs):
""" """
request = args[1] request = args[1]
req_roles = set(roles) req_roles = set(roles)
if not request.keystone_user.get('authenticated', False): if not request.keystone_user.get("authenticated", False):
return Response({'errors': ["Credentials incorrect or none given."]}, return Response({"errors": ["Credentials incorrect or none given."]}, 401)
401)
roles = set(request.keystone_user.get('roles', [])) roles = set(request.keystone_user.get("roles", []))
if roles & req_roles: if roles & req_roles:
return func(*args, **kwargs) return func(*args, **kwargs)
return Response({'errors': ["Must have one of the following roles: %s" % return Response(
list(req_roles)]}, 403) {"errors": ["Must have one of the following roles: %s" % list(req_roles)]}, 403
)
@decorator @decorator
@ -47,7 +47,8 @@ def mod_or_admin(func, *args, **kwargs):
Admin is allowed everything, so is also included. Admin is allowed everything, so is also included.
""" """
return require_roles( return require_roles(
{'project_admin', 'project_mod', 'admin'}, func, *args, **kwargs) {"project_admin", "project_mod", "admin"}, func, *args, **kwargs
)
@decorator @decorator
@ -55,8 +56,7 @@ def project_admin(func, *args, **kwargs):
""" """
endpoints setup with this decorator require the admin/project admin role. endpoints setup with this decorator require the admin/project admin role.
""" """
return require_roles( return require_roles({"project_admin", "admin"}, func, *args, **kwargs)
{'project_admin', 'admin'}, func, *args, **kwargs)
@decorator @decorator
@ -64,8 +64,7 @@ def admin(func, *args, **kwargs):
""" """
endpoints setup with this decorator require the admin role. endpoints setup with this decorator require the admin role.
""" """
return require_roles( return require_roles({"admin"}, func, *args, **kwargs)
{'admin'}, func, *args, **kwargs)
@decorator @decorator
@ -74,9 +73,8 @@ def authenticated(func, *args, **kwargs):
endpoints setup with this decorator require the user to be signed in endpoints setup with this decorator require the user to be signed in
""" """
request = args[1] request = args[1]
if not request.keystone_user.get('authenticated', False): if not request.keystone_user.get("authenticated", False):
return Response({'errors': ["Credentials incorrect or none given."]}, return Response({"errors": ["Credentials incorrect or none given."]}, 401)
401)
return func(*args, **kwargs) 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. Make a function (or API call) take at least some time.
""" """
# doesn't apply during tests # doesn't apply during tests
if 'test' in sys.argv: if "test" in sys.argv:
return func(*args, **kwargs) return func(*args, **kwargs)
start = datetime.utcnow() start = datetime.utcnow()

View File

@ -30,15 +30,15 @@ from adjutant.config import CONF
class UserList(tasks.InviteUser): class UserList(tasks.InviteUser):
url = r'^openstack/users/?$' url = r"^openstack/users/?$"
config_group = groups.DynamicNameConfigGroup( config_group = groups.DynamicNameConfigGroup(
children=[ children=[
fields.ListConfig( fields.ListConfig(
'blacklisted_roles', "blacklisted_roles",
help_text="Users with any of these roles will be hidden from the user list.", help_text="Users with any of these roles will be hidden from the user list.",
default=[], default=[],
sample_default=['admin'] sample_default=["admin"],
), ),
] ]
) )
@ -51,11 +51,12 @@ class UserList(tasks.InviteUser):
user_list = [] user_list = []
id_manager = user_store.IdentityManager() 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) project = id_manager.get_project(project_id)
can_manage_roles = id_manager.get_manageable_roles( can_manage_roles = id_manager.get_manageable_roles(
request.keystone_user['roles']) request.keystone_user["roles"]
)
active_emails = set() active_emails = set()
for user in id_manager.list_users(project): for user in id_manager.list_users(project):
@ -77,20 +78,22 @@ class UserList(tasks.InviteUser):
if skip: if skip:
continue continue
email = getattr(user, 'email', '') email = getattr(user, "email", "")
enabled = user.enabled enabled = user.enabled
user_status = 'Active' if enabled else 'Account Disabled' user_status = "Active" if enabled else "Account Disabled"
active_emails.add(email) active_emails.add(email)
user_list.append({ user_list.append(
'id': user.id, {
'name': user.name, "id": user.id,
'email': email, "name": user.name,
'roles': roles, "email": email,
'inherited_roles': inherited_roles, "roles": roles,
'cohort': 'Member', "inherited_roles": inherited_roles,
'status': user_status, "cohort": "Member",
'manageable': set(can_manage_roles).issuperset(roles), "status": user_status,
}) "manageable": set(can_manage_roles).issuperset(roles),
}
)
for user in id_manager.list_inherited_users(project): for user in id_manager.list_inherited_users(project):
skip = False skip = False
@ -103,25 +106,29 @@ class UserList(tasks.InviteUser):
if skip: if skip:
continue continue
email = getattr(user, 'email', '') email = getattr(user, "email", "")
enabled = user.enabled enabled = user.enabled
user_status = 'Active' if enabled else 'Account Disabled' user_status = "Active" if enabled else "Account Disabled"
user_list.append({'id': user.id, user_list.append(
'name': user.name, {
'email': email, "id": user.id,
'roles': roles, "name": user.name,
'inherited_roles': [], "email": email,
'cohort': 'Inherited', "roles": roles,
'status': user_status, "inherited_roles": [],
'manageable': False, "cohort": "Inherited",
}) "status": user_status,
"manageable": False,
}
)
# Get my active tasks for this project: # Get my active tasks for this project:
project_tasks = models.Task.objects.filter( project_tasks = models.Task.objects.filter(
project_id=project_id, project_id=project_id,
task_type="invite_user_to_project", task_type="invite_user_to_project",
completed=0, completed=0,
cancelled=0) cancelled=0,
)
registrations = [] registrations = []
for task in project_tasks: for task in project_tasks:
@ -143,7 +150,8 @@ class UserList(tasks.InviteUser):
task_data.update(action.action_data) task_data.update(action.action_data)
registrations.append( registrations.append(
{'uuid': task.uuid, 'task_data': task_data, 'status': status}) {"uuid": task.uuid, "task_data": task_data, "status": status}
)
for task in registrations: for task in registrations:
# NOTE(adriant): commenting out for now as it causes more confusion # NOTE(adriant): commenting out for now as it causes more confusion
@ -151,34 +159,33 @@ class UserList(tasks.InviteUser):
# measures are in place. # measures are in place.
# if task['task_data']['email'] not in active_emails: # if task['task_data']['email'] not in active_emails:
user = { user = {
'id': task['uuid'], "id": task["uuid"],
'name': task['task_data']['email'], "name": task["task_data"]["email"],
'email': task['task_data']['email'], "email": task["task_data"]["email"],
'roles': task['task_data']['roles'], "roles": task["task_data"]["roles"],
'inherited_roles': "inherited_roles": task["task_data"]["inherited_roles"],
task['task_data']['inherited_roles'], "cohort": "Invited",
'cohort': 'Invited', "status": task["status"],
'status': task['status']
} }
if not CONF.identity.username_is_email: if not CONF.identity.username_is_email:
user['name'] = task['task_data']['username'] user["name"] = task["task_data"]["username"]
user_list.append(user) user_list.append(user)
return Response({'users': user_list}) return Response({"users": user_list})
class UserDetail(BaseDelegateAPI): class UserDetail(BaseDelegateAPI):
url = r'^openstack/users/(?P<user_id>\w+)/?$' url = r"^openstack/users/(?P<user_id>\w+)/?$"
config_group = groups.DynamicNameConfigGroup( config_group = groups.DynamicNameConfigGroup(
children=[ children=[
fields.ListConfig( fields.ListConfig(
'blacklisted_roles', "blacklisted_roles",
help_text="User with these roles will return not found.", help_text="User with these roles will return not found.",
default=[], default=[],
sample_default=['admin'] sample_default=["admin"],
), ),
] ]
) )
@ -193,30 +200,34 @@ class UserDetail(BaseDelegateAPI):
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
user = id_manager.get_user(user_id) 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: if not user:
return Response(no_user, status=404) return Response(no_user, status=404)
class_conf = self.config class_conf = self.config
blacklisted_roles = class_conf.blacklisted_roles 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) project = id_manager.get_project(project_id)
roles = [role.name for role in id_manager.get_roles(user, project)] roles = [role.name for role in id_manager.get_roles(user, project)]
roles_blacklisted = set(blacklisted_roles) & set(roles) roles_blacklisted = set(blacklisted_roles) & set(roles)
inherited_roles = [ inherited_roles = [
role.name for role in id_manager.get_roles(user, project, True)] role.name for role in id_manager.get_roles(user, project, True)
inherited_roles_blacklisted = ( ]
set(blacklisted_roles) & set(inherited_roles)) inherited_roles_blacklisted = set(blacklisted_roles) & set(inherited_roles)
if not roles or roles_blacklisted or inherited_roles_blacklisted: if not roles or roles_blacklisted or inherited_roles_blacklisted:
return Response(no_user, status=404) return Response(no_user, status=404)
return Response({'id': user.id, return Response(
"username": user.name, {
"email": getattr(user, 'email', ''), "id": user.id,
'roles': roles, "username": user.name,
'inherited_roles': inherited_roles}) "email": getattr(user, "email", ""),
"roles": roles,
"inherited_roles": inherited_roles,
}
)
@utils.mod_or_admin @utils.mod_or_admin
def delete(self, request, user_id): def delete(self, request, user_id):
@ -226,37 +237,42 @@ class UserDetail(BaseDelegateAPI):
""" """
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
user = id_manager.get_user(user_id) 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. # NOTE(dale): For now, we only support cancelling pending invites.
if user: if user:
return Response( return Response(
{'errors': [ {
'Revoking keystone users not implemented. ' "errors": [
'Try removing all roles instead.']}, "Revoking keystone users not implemented. "
status=501) "Try removing all roles instead."
]
},
status=501,
)
project_tasks = models.Task.objects.filter( project_tasks = models.Task.objects.filter(
project_id=project_id, project_id=project_id,
task_type="invite_user_to_project", task_type="invite_user_to_project",
completed=0, completed=0,
cancelled=0) cancelled=0,
)
for task in project_tasks: for task in project_tasks:
if task.uuid == user_id: if task.uuid == user_id:
self.task_manager.cancel(task) self.task_manager.cancel(task)
return Response('Cancelled pending invite task!', status=200) return Response("Cancelled pending invite task!", status=200)
return Response('Not found.', status=404) return Response("Not found.", status=404)
class UserRoles(BaseDelegateAPI): 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( config_group = groups.DynamicNameConfigGroup(
children=[ children=[
fields.ListConfig( fields.ListConfig(
'blacklisted_roles', "blacklisted_roles",
help_text="User with these roles will return not found.", help_text="User with these roles will return not found.",
default=[], default=[],
sample_default=['admin'] sample_default=["admin"],
), ),
] ]
) )
@ -269,11 +285,11 @@ class UserRoles(BaseDelegateAPI):
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
user = id_manager.get_user(user_id) 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: if not user:
return Response(no_user, status=404) 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) project = id_manager.get_project(project_id)
class_conf = self.config class_conf = self.config
@ -282,19 +298,18 @@ class UserRoles(BaseDelegateAPI):
roles = [role.name for role in id_manager.get_roles(user, project)] roles = [role.name for role in id_manager.get_roles(user, project)]
roles_blacklisted = set(blacklisted_roles) & set(roles) roles_blacklisted = set(blacklisted_roles) & set(roles)
inherited_roles = [ inherited_roles = [
role.name for role in id_manager.get_roles(user, project, True)] role.name for role in id_manager.get_roles(user, project, True)
inherited_roles_blacklisted = ( ]
set(blacklisted_roles) & set(inherited_roles)) inherited_roles_blacklisted = set(blacklisted_roles) & set(inherited_roles)
if not roles or roles_blacklisted or inherited_roles_blacklisted: if not roles or roles_blacklisted or inherited_roles_blacklisted:
return Response(no_user, status=404) return Response(no_user, status=404)
return Response({'roles': roles, return Response({"roles": roles, "inherited_roles": inherited_roles})
'inherited_roles': inherited_roles})
@utils.mod_or_admin @utils.mod_or_admin
def put(self, args, **kwargs): def put(self, args, **kwargs):
""" Add user roles to the current project. """ """ Add user roles to the current project. """
kwargs['remove_role'] = False kwargs["remove_role"] = False
return self._edit_user(args, **kwargs) return self._edit_user(args, **kwargs)
@utils.mod_or_admin @utils.mod_or_admin
@ -303,34 +318,35 @@ class UserRoles(BaseDelegateAPI):
This only supports Active users This only supports Active users
""" """
kwargs['remove_role'] = True kwargs["remove_role"] = True
return self._edit_user(args, **kwargs) return self._edit_user(args, **kwargs)
def _edit_user(self, request, user_id, remove_role=False, format=None): def _edit_user(self, request, user_id, remove_role=False, format=None):
""" Helper function to add or remove roles from a user """ """ Helper function to add or remove roles from a user """
request.data['remove'] = remove_role request.data["remove"] = remove_role
if 'project_id' not in request.data: if "project_id" not in request.data:
request.data['project_id'] = request.keystone_user['project_id'] request.data["project_id"] = request.keystone_user["project_id"]
request.data['user_id'] = user_id request.data["user_id"] = user_id
self.logger.info("(%s) - New EditUser %s request." % ( self.logger.info(
timezone.now(), request.method)) "(%s) - New EditUser %s request." % (timezone.now(), request.method)
)
self.task_manager.create_from_request(self.task_type, request) 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): class RoleList(BaseDelegateAPI):
url = r'^openstack/roles/?$' url = r"^openstack/roles/?$"
@utils.mod_or_admin @utils.mod_or_admin
def get(self, request): def get(self, request):
"""Returns a list of roles that may be managed for this project""" """Returns a list of roles that may be managed for this project"""
# get roles for this user on the 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() id_manager = user_store.IdentityManager()
manageable_role_names = id_manager.get_manageable_roles(user_roles) manageable_role_names = id_manager.get_manageable_roles(user_roles)
@ -342,7 +358,7 @@ class RoleList(BaseDelegateAPI):
if role: if role:
manageable_roles.append(role.to_dict()) manageable_roles.append(role.to_dict())
return Response({'roles': manageable_roles}) return Response({"roles": manageable_roles})
class UserResetPassword(tasks.ResetPassword): 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 pass
@ -362,7 +378,7 @@ class UserUpdateEmail(tasks.UpdateEmail):
--- ---
""" """
url = r'^openstack/users/email-update/?$' url = r"^openstack/users/email-update/?$"
pass pass
@ -372,7 +388,7 @@ class SignUp(tasks.CreateProjectAndUser):
The openstack endpoint for signups. The openstack endpoint for signups.
""" """
url = r'^openstack/sign-up/?$' url = r"^openstack/sign-up/?$"
pass pass
@ -383,7 +399,7 @@ class UpdateProjectQuotas(BaseDelegateAPI):
one or more regions one or more regions
""" """
url = r'^openstack/quotas/?$' url = r"^openstack/quotas/?$"
task_type = "update_quota" task_type = "update_quota"
@ -395,7 +411,7 @@ class UpdateProjectQuotas(BaseDelegateAPI):
task_type__exact=self.task_type, task_type__exact=self.task_type,
project_id__exact=self.project_id, project_id__exact=self.project_id,
cancelled=0, cancelled=0,
).order_by('-created_on')[:self._number_of_returned_tasks] ).order_by("-created_on")[: self._number_of_returned_tasks]
response_tasks = [] response_tasks = []
@ -409,13 +425,12 @@ class UpdateProjectQuotas(BaseDelegateAPI):
task_data.update(action.action_data) task_data.update(action.action_data)
new_dict = { new_dict = {
"id": task.uuid, "id": task.uuid,
"regions": task_data['regions'], "regions": task_data["regions"],
"size": task_data['size'], "size": task_data["size"],
"request_user": "request_user": task.keystone_user["username"],
task.keystone_user['username'],
"task_created": task.created_on, "task_created": task.created_on,
"valid": all([a.valid for a in task.actions]), "valid": all([a.valid for a in task.actions]),
"status": status "status": status,
} }
response_tasks.append(new_dict) response_tasks.append(new_dict)
@ -439,9 +454,9 @@ class UpdateProjectQuotas(BaseDelegateAPI):
quota_sizes = CONF.quota.sizes quota_sizes = CONF.quota.sizes
size_order = CONF.quota.sizes_ascending size_order = CONF.quota.sizes_ascending
self.project_id = request.keystone_user['project_id'] self.project_id = request.keystone_user["project_id"]
regions = request.query_params.get('regions', None) regions = request.query_params.get("regions", None)
include_usage = request.query_params.get('include_usage', True) include_usage = request.query_params.get("include_usage", True)
if regions: if regions:
regions = regions.split(",") regions = regions.split(",")
@ -456,35 +471,38 @@ class UpdateProjectQuotas(BaseDelegateAPI):
quota_manager = QuotaManager(self.project_id) quota_manager = QuotaManager(self.project_id)
for region in regions: for region in regions:
if self.check_region_exists(region): if self.check_region_exists(region):
region_quotas.append(quota_manager.get_region_quota_data( region_quotas.append(
region, include_usage)) quota_manager.get_region_quota_data(region, include_usage)
)
else: else:
return Response( return Response({"ERROR": ["Region: %s is not valid" % region]}, 400)
{"ERROR": ['Region: %s is not valid' % region]}, 400)
response_tasks = self.get_active_quota_tasks() response_tasks = self.get_active_quota_tasks()
return Response({'regions': region_quotas, return Response(
"quota_sizes": quota_sizes, {
"quota_size_order": size_order, "regions": region_quotas,
"active_quota_tasks": response_tasks}) "quota_sizes": quota_sizes,
"quota_size_order": size_order,
"active_quota_tasks": response_tasks,
}
)
@utils.mod_or_admin @utils.mod_or_admin
def post(self, request): def post(self, request):
request.data['project_id'] = request.keystone_user['project_id'] request.data["project_id"] = request.keystone_user["project_id"]
self.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: if not regions:
id_manager = user_store.IdentityManager() id_manager = user_store.IdentityManager()
regions = [region.id for region in id_manager.list_regions()] 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." self.logger.info("(%s) - New UpdateProjectQuotas request." % timezone.now())
% timezone.now())
self.task_manager.create_from_request(self.task_type, request) 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 # NOTE(adriant): We should deprecate these Views properly and switch tests
# to work against the openstack ones. # to work against the openstack ones.
class CreateProjectAndUser(BaseDelegateAPI): class CreateProjectAndUser(BaseDelegateAPI):
url = r'^actions/CreateProjectAndUser/?$' url = r"^actions/CreateProjectAndUser/?$"
config_group = groups.DynamicNameConfigGroup( config_group = groups.DynamicNameConfigGroup(
children=[ children=[
fields.StrConfig( fields.StrConfig(
'default_region', "default_region",
help_text="Default region in which any potential resources may be created.", help_text="Default region in which any potential resources may be created.",
required=True, required=True,
default="RegionOne", default="RegionOne",
@ -48,9 +49,9 @@ class CreateProjectAndUser(BaseDelegateAPI):
fields.StrConfig( fields.StrConfig(
"default_parent_id", "default_parent_id",
help_text="Parent id under which this project will be created. " 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, default=None,
) ),
] ]
) )
@ -64,28 +65,27 @@ class CreateProjectAndUser(BaseDelegateAPI):
incoming data and create a task to be approved incoming data and create a task to be approved
later. later.
""" """
self.logger.info( self.logger.info("(%s) - Starting new project task." % timezone.now())
"(%s) - Starting new project task." % timezone.now())
class_conf = self.config class_conf = self.config
# we need to set the region the resources will be created in: # 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 # 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: # 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) 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): class InviteUser(BaseDelegateAPI):
url = r'^actions/InviteUser/?$' url = r"^actions/InviteUser/?$"
task_type = "invite_user_to_project" task_type = "invite_user_to_project"
@ -105,24 +105,21 @@ class InviteUser(BaseDelegateAPI):
self.logger.info("(%s) - New AttachUser request." % timezone.now()) self.logger.info("(%s) - New AttachUser request." % timezone.now())
# Default project_id to the keystone user's project # Default project_id to the keystone user's project
if ('project_id' not in request.data if "project_id" not in request.data or request.data["project_id"] is None:
or request.data['project_id'] is None): request.data["project_id"] = request.keystone_user["project_id"]
request.data['project_id'] = request.keystone_user['project_id']
# Default domain_id to the keystone user's project # Default domain_id to the keystone user's project
if ('domain_id' not in request.data if "domain_id" not in request.data or request.data["domain_id"] is None:
or request.data['domain_id'] is None): request.data["domain_id"] = request.keystone_user["project_domain_id"]
request.data['domain_id'] = \
request.keystone_user['project_domain_id']
self.task_manager.create_from_request(self.task_type, request) 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): class ResetPassword(BaseDelegateAPI):
url = r'^actions/ResetPassword/?$' url = r"^actions/ResetPassword/?$"
task_type = "reset_user_password" task_type = "reset_user_password"
@ -156,17 +153,19 @@ class ResetPassword(BaseDelegateAPI):
self.task_manager.create_from_request(self.task_type, request) self.task_manager.create_from_request(self.task_type, request)
except exceptions.BaseTaskException as e: except exceptions.BaseTaskException as e:
self.logger.info( self.logger.info(
"(%s) - ResetPassword raised error: %s" % (timezone.now(), e)) "(%s) - ResetPassword raised error: %s" % (timezone.now(), e)
)
response_dict = {'notes': [ response_dict = {
"If user with email exists, reset token will be issued."]} "notes": ["If user with email exists, reset token will be issued."]
}
return Response(response_dict, status=202) return Response(response_dict, status=202)
class EditUser(BaseDelegateAPI): class EditUser(BaseDelegateAPI):
url = r'^actions/EditUser/?$' url = r"^actions/EditUser/?$"
task_type = "edit_user_roles" task_type = "edit_user_roles"
@ -183,12 +182,12 @@ class EditUser(BaseDelegateAPI):
self.task_manager.create_from_request(self.task_type, request) 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): class UpdateEmail(BaseDelegateAPI):
url = r'^actions/UpdateEmail/?$' url = r"^actions/UpdateEmail/?$"
task_type = "update_user_email" task_type = "update_user_email"
@ -199,8 +198,8 @@ class UpdateEmail(BaseDelegateAPI):
This will submit and approve an update email action. 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) 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 from adjutant.config import CONF
urlpatterns = [ urlpatterns = [
url(r'^status/?$', views.StatusView.as_view()), url(r"^status/?$", views.StatusView.as_view()),
url(r'^tasks/(?P<uuid>\w+)/?$', views.TaskDetail.as_view()), url(r"^tasks/(?P<uuid>\w+)/?$", views.TaskDetail.as_view()),
url(r'^tasks/?$', views.TaskList.as_view()), url(r"^tasks/?$", views.TaskList.as_view()),
url(r'^tokens/(?P<id>\w+)', views.TokenDetail.as_view()), url(r"^tokens/(?P<id>\w+)", views.TokenDetail.as_view()),
url(r'^tokens/?$', views.TokenList.as_view()), url(r"^tokens/?$", views.TokenList.as_view()),
url(r'^notifications/(?P<uuid>\w+)/?$', url(r"^notifications/(?P<uuid>\w+)/?$", views.NotificationDetail.as_view()),
views.NotificationDetail.as_view()), url(r"^notifications/?$", views.NotificationList.as_view()),
url(r'^notifications/?$', views.NotificationList.as_view()),
] ]
for active_view in CONF.api.active_delegate_apis: for active_view in CONF.api.active_delegate_apis:
delegate_api = api.DELEGATE_API_CLASSES[active_view] delegate_api = api.DELEGATE_API_CLASSES[active_view]
urlpatterns.append( urlpatterns.append(url(delegate_api.url, delegate_api.as_view()))
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! BE AWARE! WILL NOT WORK UNLESS POSITIONAL ARGUMENT 3 IS FILTERS!
""" """
request = args[1] request = args[1]
filters = request.query_params.get('filters', None) filters = request.query_params.get("filters", None)
if not filters: if not filters:
return func(*args, **kwargs) return func(*args, **kwargs)
@ -40,14 +40,16 @@ def parse_filters(func, *args, **kwargs):
filters = json.loads(filters) filters = json.loads(filters)
for field, operations in filters.items(): for field, operations in filters.items():
for operation, value in operations.items(): for operation, value in operations.items():
cleaned_filters['%s__%s' % (field, operation)] = value cleaned_filters["%s__%s" % (field, operation)] = value
except (ValueError, AttributeError): except (ValueError, AttributeError):
return Response( return Response(
{'errors': [ {
"Filters incorrectly formatted. Required format: " "errors": [
"{'filters': {'fieldname': { 'operation': 'value'}}" "Filters incorrectly formatted. Required format: "
]}, "{'filters': {'fieldname': { 'operation': 'value'}}"
status=400 ]
},
status=400,
) )
try: try:
@ -57,4 +59,4 @@ def parse_filters(func, *args, **kwargs):
args[2] = cleaned_filters args[2] = cleaned_filters
return func(*args, **kwargs) return func(*args, **kwargs)
except FieldError as e: 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): class V1VersionEndpoint(SingleVersionView):
version = '1.0' version = "1.0"
class APIViewWithLogger(APIView): class APIViewWithLogger(APIView):
""" """
APIView with a logger. APIView with a logger.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(APIViewWithLogger, self).__init__(*args, **kwargs) super(APIViewWithLogger, self).__init__(*args, **kwargs)
self.logger = getLogger('adjutant') self.logger = getLogger("adjutant")
self.task_manager = TaskManager() self.task_manager = TaskManager()
class StatusView(APIViewWithLogger): class StatusView(APIViewWithLogger):
@utils.admin @utils.admin
def get(self, request, filters=None, format=None): def get(self, request, filters=None, format=None):
""" """
@ -56,33 +56,31 @@ class StatusView(APIViewWithLogger):
Can returns None, if there are no tasks. Can returns None, if there are no tasks.
""" """
notifications = Notification.objects.filter( notifications = Notification.objects.filter(error=1, acknowledged=0)
error=1,
acknowledged=0
)
try: try:
last_created_task = Task.objects.filter( last_created_task = (
completed=0).order_by("-created_on")[0].to_dict() Task.objects.filter(completed=0).order_by("-created_on")[0].to_dict()
)
except IndexError: except IndexError:
last_created_task = None last_created_task = None
try: try:
last_completed_task = Task.objects.filter( last_completed_task = (
completed=1).order_by("-completed_on")[0].to_dict() Task.objects.filter(completed=1).order_by("-completed_on")[0].to_dict()
)
except IndexError: except IndexError:
last_completed_task = None last_completed_task = None
status = { status = {
"error_notifications": [note.to_dict() for note in notifications], "error_notifications": [note.to_dict() for note in notifications],
"last_created_task": last_created_task, "last_created_task": last_created_task,
"last_completed_task": last_completed_task "last_completed_task": last_completed_task,
} }
return Response(status, status=200) return Response(status, status=200)
class NotificationList(APIViewWithLogger): class NotificationList(APIViewWithLogger):
@utils.admin @utils.admin
@parse_filters @parse_filters
def get(self, request, filters=None, format=None): def get(self, request, filters=None, format=None):
@ -90,33 +88,37 @@ class NotificationList(APIViewWithLogger):
A list of Notification objects as dicts. A list of Notification objects as dicts.
""" """
if filters: if filters:
notifications = Notification.objects.filter( notifications = Notification.objects.filter(**filters).order_by(
**filters).order_by("-created_on") "-created_on"
)
else: else:
notifications = Notification.objects.all().order_by("-created_on") notifications = Notification.objects.all().order_by("-created_on")
page = request.GET.get('page', 1) page = request.GET.get("page", 1)
notifs_per_page = request.GET.get('notifications_per_page', None) notifs_per_page = request.GET.get("notifications_per_page", None)
if notifs_per_page: if notifs_per_page:
paginator = Paginator(notifications, notifs_per_page) paginator = Paginator(notifications, notifs_per_page)
try: try:
notifications = paginator.page(page) notifications = paginator.page(page)
except EmptyPage: except EmptyPage:
return Response({'errors': ['Empty page']}, status=400) return Response({"errors": ["Empty page"]}, status=400)
except PageNotAnInteger: except PageNotAnInteger:
return Response({'errors': ['Page not an integer']}, return Response({"errors": ["Page not an integer"]}, status=400)
status=400)
note_list = [] note_list = []
for notification in notifications: for notification in notifications:
note_list.append(notification.to_dict()) note_list.append(notification.to_dict())
if notifs_per_page: if notifs_per_page:
return Response({'notifications': note_list, return Response(
'pages': paginator.num_pages, {
'has_more': notifications.has_next(), "notifications": note_list,
'has_prev': notifications.has_previous()}, "pages": paginator.num_pages,
status=200) "has_more": notifications.has_next(),
"has_prev": notifications.has_previous(),
},
status=200,
)
return Response({"notifications": note_list}, status=200) return Response({"notifications": note_list}, status=200)
@ -125,24 +127,21 @@ class NotificationList(APIViewWithLogger):
""" """
Acknowledge notifications. Acknowledge notifications.
""" """
note_list = request.data.get('notifications', None) note_list = request.data.get("notifications", None)
if note_list and isinstance(note_list, list): if note_list and isinstance(note_list, list):
notifications = Notification.objects.filter(uuid__in=note_list) notifications = Notification.objects.filter(uuid__in=note_list)
for notification in notifications: for notification in notifications:
notification.acknowledged = True notification.acknowledged = True
notification.save() notification.save()
return Response({'notes': ['Notifications acknowledged.']}, return Response({"notes": ["Notifications acknowledged."]}, status=200)
status=200)
else: else:
return Response( return Response(
{'notifications': [ {"notifications": ["this field is required and needs to be a list."]},
"this field is required and needs to be a list." status=400,
]}, )
status=400)
class NotificationDetail(APIViewWithLogger): class NotificationDetail(APIViewWithLogger):
@utils.admin @utils.admin
def get(self, request, uuid, format=None): def get(self, request, uuid, format=None):
""" """
@ -152,9 +151,7 @@ class NotificationDetail(APIViewWithLogger):
try: try:
notification = Notification.objects.get(uuid=uuid) notification = Notification.objects.get(uuid=uuid)
except Notification.DoesNotExist: except Notification.DoesNotExist:
return Response( return Response({"errors": ["No notification with this id."]}, status=404)
{'errors': ['No notification with this id.']},
status=404)
return Response(notification.to_dict()) return Response(notification.to_dict())
@utils.admin @utils.admin
@ -165,25 +162,21 @@ class NotificationDetail(APIViewWithLogger):
try: try:
notification = Notification.objects.get(uuid=uuid) notification = Notification.objects.get(uuid=uuid)
except Notification.DoesNotExist: except Notification.DoesNotExist:
return Response( return Response({"errors": ["No notification with this id."]}, status=404)
{'errors': ['No notification with this id.']},
status=404)
if notification.acknowledged: if notification.acknowledged:
return Response({'notes': ['Notification already acknowledged.']}, return Response(
status=200) {"notes": ["Notification already acknowledged."]}, status=200
if request.data.get('acknowledged', False) is True: )
if request.data.get("acknowledged", False) is True:
notification.acknowledged = True notification.acknowledged = True
notification.save() notification.save()
return Response({'notes': ['Notification acknowledged.']}, return Response({"notes": ["Notification acknowledged."]}, status=200)
status=200)
else: else:
return Response({'acknowledged': ["this field is required."]}, return Response({"acknowledged": ["this field is required."]}, status=400)
status=400)
class TaskList(APIViewWithLogger): class TaskList(APIViewWithLogger):
@utils.admin @utils.admin
@parse_filters @parse_filters
def get(self, request, filters=None, format=None): def get(self, request, filters=None, format=None):
@ -192,20 +185,20 @@ class TaskList(APIViewWithLogger):
and their related actions. and their related actions.
""" """
page = request.GET.get('page', 1) page = request.GET.get("page", 1)
tasks_per_page = request.GET.get('tasks_per_page', None) tasks_per_page = request.GET.get("tasks_per_page", None)
if not filters: if not filters:
filters = {} filters = {}
# TODO(adriant): better handle this bit of incode policy # 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 # Ignore any filters with project_id in them
for field_filter in filters.keys(): for field_filter in filters.keys():
if "project_id" in field_filter: if "project_id" in field_filter:
filters.pop(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") tasks = Task.objects.filter(**filters).order_by("-created_on")
@ -214,27 +207,30 @@ class TaskList(APIViewWithLogger):
try: try:
tasks = paginator.page(page) tasks = paginator.page(page)
except EmptyPage: except EmptyPage:
return Response({'errors': ['Empty page']}, status=400) return Response({"errors": ["Empty page"]}, status=400)
except PageNotAnInteger: except PageNotAnInteger:
return Response({'errors': ['Page not an integer']}, return Response({"errors": ["Page not an integer"]}, status=400)
status=400)
task_list = [] task_list = []
for task in tasks: for task in tasks:
task_list.append(task.to_dict()) task_list.append(task.to_dict())
if tasks_per_page: if tasks_per_page:
return Response({'tasks': task_list, return Response(
'pages': paginator.num_pages, {
'has_more': tasks.has_next(), "tasks": task_list,
'has_prev': tasks.has_previous()}, status=200) "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 # NOTE(amelia): 'has_more'and 'has_prev' names are
# based on the horizon pagination table pagination names # based on the horizon pagination table pagination names
else: else:
return Response({'tasks': task_list}) return Response({"tasks": task_list})
class TaskDetail(APIViewWithLogger): class TaskDetail(APIViewWithLogger):
@utils.mod_or_admin @utils.mod_or_admin
def get(self, request, uuid, format=None): def get(self, request, uuid, format=None):
""" """
@ -243,16 +239,15 @@ class TaskDetail(APIViewWithLogger):
""" """
try: try:
# TODO(adriant): better handle this bit of incode policy # 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) task = Task.objects.get(uuid=uuid)
else: else:
task = Task.objects.get( 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()) return Response(task.to_dict())
except Task.DoesNotExist: except Task.DoesNotExist:
return Response( return Response({"errors": ["No task with this id."]}, status=404)
{'errors': ['No task with this id.']},
status=404)
@utils.admin @utils.admin
def put(self, request, uuid, format=None): def put(self, request, uuid, format=None):
@ -262,9 +257,7 @@ class TaskDetail(APIViewWithLogger):
""" """
self.task_manager.update(uuid, request.data) self.task_manager.update(uuid, request.data)
return Response( return Response({"notes": ["Task successfully updated."]}, status=200)
{'notes': ["Task successfully updated."]},
status=200)
@utils.admin @utils.admin
def post(self, request, uuid, format=None): def post(self, request, uuid, format=None):
@ -274,21 +267,21 @@ class TaskDetail(APIViewWithLogger):
and if valid will setup and create a related token. and if valid will setup and create a related token.
""" """
try: try:
if request.data.get('approved') is not True: if request.data.get("approved") is not True:
raise exceptions.TaskSerializersInvalid( raise exceptions.TaskSerializersInvalid(
{'approved': ["this is a required boolean field."]}) {"approved": ["this is a required boolean field."]}
)
except ParseError: except ParseError:
raise exceptions.TaskSerializersInvalid( 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) task = self.task_manager.approve(uuid, request.keystone_user)
if task.completed: if task.completed:
return Response( return Response({"notes": ["Task completed successfully."]}, status=200)
{'notes': ["Task completed successfully."]}, status=200)
else: else:
return Response( return Response({"notes": ["created token"]}, status=202)
{'notes': ['created token']}, status=202)
@utils.mod_or_admin @utils.mod_or_admin
def delete(self, request, uuid, format=None): def delete(self, request, uuid, format=None):
@ -300,21 +293,18 @@ class TaskDetail(APIViewWithLogger):
""" """
try: try:
# TODO(adriant): better handle this bit of incode policy # 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) task = Task.objects.get(uuid=uuid)
else: else:
task = Task.objects.get( 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: except Task.DoesNotExist:
return Response( return Response({"errors": ["No task with this id."]}, status=404)
{'errors': ['No task with this id.']},
status=404)
self.task_manager.cancel(task) self.task_manager.cancel(task)
return Response( return Response({"notes": ["Task cancelled successfully."]}, status=200)
{'notes': ["Task cancelled successfully."]},
status=200)
class TokenList(APIViewWithLogger): class TokenList(APIViewWithLogger):
@ -344,26 +334,24 @@ class TokenList(APIViewWithLogger):
Clears other tokens for it. Clears other tokens for it.
""" """
uuid = request.data.get('task', None) uuid = request.data.get("task", None)
if uuid is None: if uuid is None:
return Response( return Response(
{'errors': {'task': ["This field is required.", ]}}, {"errors": {"task": ["This field is required.",]}}, status=400
status=400) )
try: try:
# TODO(adriant): better handle this bit of incode policy # 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) task = Task.objects.get(uuid=uuid)
else: else:
task = Task.objects.get( 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: except Task.DoesNotExist:
return Response( return Response({"errors": ["No task with this id."]}, status=404)
{'errors': ['No task with this id.']},
status=404)
self.task_manager.reissue_token(task) self.task_manager.reissue_token(task)
return Response( return Response({"notes": ["Token reissued."]}, status=200)
{'notes': ['Token reissued.']}, status=200)
@utils.admin @utils.admin
def delete(self, request, format=None): def delete(self, request, format=None):
@ -372,12 +360,10 @@ class TokenList(APIViewWithLogger):
""" """
now = timezone.now() now = timezone.now()
Token.objects.filter(expires__lt=now).delete() Token.objects.filter(expires__lt=now).delete()
return Response( return Response({"notes": ["Deleted all expired tokens."]}, status=200)
{'notes': ['Deleted all expired tokens.']}, status=200)
class TokenDetail(APIViewWithLogger): class TokenDetail(APIViewWithLogger):
def get(self, request, id, format=None): def get(self, request, id, format=None):
""" """
Returns a response with the list of required fields Returns a response with the list of required fields
@ -390,20 +376,16 @@ class TokenDetail(APIViewWithLogger):
token = Token.objects.get(token=id) token = Token.objects.get(token=id)
except Token.DoesNotExist: except Token.DoesNotExist:
return Response( return Response(
{'errors': ['This token does not exist or has expired.']}, {"errors": ["This token does not exist or has expired."]}, status=404
status=404) )
if token.task.completed: if token.task.completed:
return Response( return Response(
{'errors': {"errors": ["This task has already been completed."]}, status=400
['This task has already been completed.']}, )
status=400)
if token.task.cancelled: if token.task.cancelled:
return Response( return Response({"errors": ["This task has been cancelled."]}, status=400)
{'errors':
['This task has been cancelled.']},
status=400)
required_fields = [] required_fields = []
actions = [] actions = []
@ -415,9 +397,13 @@ class TokenDetail(APIViewWithLogger):
if field not in required_fields: if field not in required_fields:
required_fields.append(field) required_fields.append(field)
return Response({'actions': [str(act) for act in actions], return Response(
'required_fields': required_fields, {
'task_type': token.task.task_type}) "actions": [str(act) for act in actions],
"required_fields": required_fields,
"task_type": token.task.task_type,
}
)
def post(self, request, id, format=None): def post(self, request, id, format=None):
""" """
@ -432,11 +418,9 @@ class TokenDetail(APIViewWithLogger):
token = Token.objects.get(token=id) token = Token.objects.get(token=id)
except Token.DoesNotExist: except Token.DoesNotExist:
return Response( return Response(
{'errors': ['This token does not exist or has expired.']}, {"errors": ["This token does not exist or has expired."]}, status=404
status=404) )
self.task_manager.submit(token.task, request.data) self.task_manager.submit(token.task, request.data)
return Response( return Response({"notes": ["Token submitted successfully."]}, status=200)
{'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 relative_endpoint = "v%s/" % int_id
mime_type = "application/vnd.openstack.adjutant-v%s+json" % int_id mime_type = "application/vnd.openstack.adjutant-v%s+json" % int_id
version_details = { version_details = {
'status': status, "status": status,
'id': id, "id": id,
'media-types': [ "media-types": [{"base": "application/json", "type": mime_type}],
{ "links": [],
'base': 'application/json',
'type': mime_type
}
],
'links': []
} }
if 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 _VERSIONS[id] = version_details
return version_details return version_details
class VersionView(APIView): class VersionView(APIView):
def get(self, request): def get(self, request):
versions = [] versions = []
for version in _VERSIONS.values(): for version in _VERSIONS.values():
version = version.copy() version = version.copy()
rel_endpoint = version.pop('relative_endpoint') rel_endpoint = version.pop("relative_endpoint")
url = request.build_absolute_uri() + rel_endpoint url = request.build_absolute_uri() + rel_endpoint
version['links'] = version['links'] + [{'href': url, version["links"] = version["links"] + [{"href": url, "rel": "self"}]
'rel': 'self'}]
versions.append(version) versions.append(version)
return Response({'versions': versions}, status=200) return Response({"versions": versions}, status=200)
class SingleVersionView(APIView): class SingleVersionView(APIView):
@ -58,11 +51,11 @@ class SingleVersionView(APIView):
version = _VERSIONS.get(self.version, {}).copy() version = _VERSIONS.get(self.version, {}).copy()
if not version: 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'] + [ version["links"] = version["links"] + [
{'href': request.build_absolute_uri(), {"href": request.build_absolute_uri(), "rel": "self"}
'rel': 'self'}] ]
return Response({'version': version}, status=200) return Response({"version": version}, status=200)

View File

@ -10,8 +10,8 @@ from adjutant import config
def make_yaml_lines(val, depth, comment=False): def make_yaml_lines(val, depth, comment=False):
new_lines = [] new_lines = []
line_prefix = " " * (depth + 1) line_prefix = " " * (depth + 1)
for line in yaml.dump(val).split('\n'): for line in yaml.dump(val).split("\n"):
if line == '': if line == "":
continue continue
if comment: if comment:
new_lines.append(line_prefix + "# %s" % line) 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_help_text = "# %s" % field.help_text
field_lines.append(line_prefix + field_help_text) field_lines.append(line_prefix + field_help_text)
default = '' default = ""
if field.default is not None: if field.default is not None:
default = field.default default = field.default
@ -48,7 +48,7 @@ def make_field_lines(field, depth):
else: else:
field_lines.append(line_prefix + "# %s:" % field.name) field_lines.append(line_prefix + "# %s:" % field.name)
else: else:
if default == '': if default == "":
field_lines.append(line_prefix + "# %s: <your_value>" % field.name) field_lines.append(line_prefix + "# %s: <your_value>" % field.name)
else: else:
default_str = " " + str(default) default_str = " " + str(default)
@ -70,20 +70,20 @@ def make_group_lines(group, depth=0):
class Command(BaseCommand): class Command(BaseCommand):
help = '' help = ""
def add_arguments(self, parser): 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): 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 = [] base_lines = []
for group in config._root_config: for group in config._root_config:
base_lines += make_group_lines(group) base_lines += make_group_lines(group)
base_lines.append("") base_lines.append("")
with open(options['output_file'], "w") as f: with open(options["output_file"], "w") as f:
for line in base_lines: for line in base_lines:
f.write(line) f.write(line)
f.write("\n") f.write("\n")

View File

@ -56,38 +56,25 @@ def get_auth_session():
def get_keystoneclient(version=DEFAULT_IDENTITY_VERSION): def get_keystoneclient(version=DEFAULT_IDENTITY_VERSION):
return ks_client.Client( return ks_client.Client(version, session=get_auth_session())
version,
session=get_auth_session())
def get_neutronclient(region): def get_neutronclient(region):
# always returns neutron client v2 # always returns neutron client v2
return neutronclient.Client( return neutronclient.Client(session=get_auth_session(), region_name=region)
session=get_auth_session(),
region_name=region)
def get_novaclient(region, version=DEFAULT_COMPUTE_VERSION): def get_novaclient(region, version=DEFAULT_COMPUTE_VERSION):
return novaclient.Client( return novaclient.Client(version, session=get_auth_session(), region_name=region)
version,
session=get_auth_session(),
region_name=region)
def get_cinderclient(region, version=DEFAULT_VOLUME_VERSION): def get_cinderclient(region, version=DEFAULT_VOLUME_VERSION):
return cinderclient.Client( return cinderclient.Client(version, session=get_auth_session(), region_name=region)
version,
session=get_auth_session(),
region_name=region)
def get_octaviaclient(region): def get_octaviaclient(region):
ks = get_keystoneclient() ks = get_keystoneclient()
service = ks.services.list(name='octavia')[0] service = ks.services.list(name="octavia")[0]
endpoint = ks.endpoints.list(service=service, endpoint = ks.endpoints.list(service=service, region=region, interface="public")[0]
region=region, interface='public')[0] return octavia.OctaviaAPI(session=get_auth_session(), endpoint=endpoint.url)
return octavia.OctaviaAPI(
session=get_auth_session(),
endpoint=endpoint.url)

View File

@ -22,7 +22,7 @@ class QuotaManager(object):
across all services. across all services.
""" """
default_size_diff_threshold = .2 default_size_diff_threshold = 0.2
class ServiceQuotaHelper(object): class ServiceQuotaHelper(object):
def set_quota(self, values): def set_quota(self, values):
@ -30,8 +30,7 @@ class QuotaManager(object):
class ServiceQuotaCinderHelper(ServiceQuotaHelper): class ServiceQuotaCinderHelper(ServiceQuotaHelper):
def __init__(self, region_name, project_id): def __init__(self, region_name, project_id):
self.client = openstack_clients.get_cinderclient( self.client = openstack_clients.get_cinderclient(region=region_name)
region=region_name)
self.project_id = project_id self.project_id = project_id
def get_quota(self): def get_quota(self):
@ -39,39 +38,40 @@ class QuotaManager(object):
def get_usage(self): def get_usage(self):
volumes = self.client.volumes.list( 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( 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 # gigabytesUsed should be a total of volumes and snapshots
gigabytes = sum([getattr(volume, 'size', 0) for volume gigabytes = sum([getattr(volume, "size", 0) for volume in volumes])
in volumes]) gigabytes += sum([getattr(snap, "size", 0) for snap in snapshots])
gigabytes += sum([getattr(snap, 'size', 0) for snap
in snapshots])
return {'gigabytes': gigabytes, return {
'volumes': len(volumes), "gigabytes": gigabytes,
'snapshots': len(snapshots) "volumes": len(volumes),
} "snapshots": len(snapshots),
}
class ServiceQuotaNovaHelper(ServiceQuotaHelper): class ServiceQuotaNovaHelper(ServiceQuotaHelper):
def __init__(self, region_name, project_id): def __init__(self, region_name, project_id):
self.client = openstack_clients.get_novaclient( self.client = openstack_clients.get_novaclient(region=region_name)
region=region_name)
self.project_id = project_id self.project_id = project_id
def get_quota(self): def get_quota(self):
return self.client.quotas.get(self.project_id).to_dict() return self.client.quotas.get(self.project_id).to_dict()
def get_usage(self): def get_usage(self):
nova_usage = self.client.limits.get( nova_usage = self.client.limits.get(tenant_id=self.project_id).to_dict()[
tenant_id=self.project_id).to_dict()['absolute'] "absolute"
]
nova_usage_keys = [ nova_usage_keys = [
('instances', 'totalInstancesUsed'), ("instances", "totalInstancesUsed"),
('floating_ips', 'totalFloatingIpsUsed'), ("floating_ips", "totalFloatingIpsUsed"),
('ram', 'totalRAMUsed'), ("ram", "totalRAMUsed"),
('cores', 'totalCoresUsed'), ("cores", "totalCoresUsed"),
('secuirty_groups', 'totalSecurityGroupsUsed') ("secuirty_groups", "totalSecurityGroupsUsed"),
] ]
nova_usage_dict = {} nova_usage_dict = {}
@ -82,53 +82,48 @@ class QuotaManager(object):
class ServiceQuotaNeutronHelper(ServiceQuotaHelper): class ServiceQuotaNeutronHelper(ServiceQuotaHelper):
def __init__(self, region_name, project_id): def __init__(self, region_name, project_id):
self.client = openstack_clients.get_neutronclient( self.client = openstack_clients.get_neutronclient(region=region_name)
region=region_name)
self.project_id = project_id self.project_id = project_id
def set_quota(self, values): def set_quota(self, values):
body = { body = {"quota": values}
'quota': values
}
self.client.update_quota(self.project_id, body) self.client.update_quota(self.project_id, body)
def get_usage(self): def get_usage(self):
networks = self.client.list_networks( networks = self.client.list_networks(tenant_id=self.project_id)["networks"]
tenant_id=self.project_id)['networks'] routers = self.client.list_routers(tenant_id=self.project_id)["routers"]
routers = self.client.list_routers( floatingips = self.client.list_floatingips(tenant_id=self.project_id)[
tenant_id=self.project_id)['routers'] "floatingips"
floatingips = self.client.list_floatingips( ]
tenant_id=self.project_id)['floatingips'] ports = self.client.list_ports(tenant_id=self.project_id)["ports"]
ports = self.client.list_ports( subnets = self.client.list_subnets(tenant_id=self.project_id)["subnets"]
tenant_id=self.project_id)['ports']
subnets = self.client.list_subnets(
tenant_id=self.project_id)['subnets']
security_groups = self.client.list_security_groups( 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( 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), return {
'router': len(routers), "network": len(networks),
'floatingip': len(floatingips), "router": len(routers),
'port': len(ports), "floatingip": len(floatingips),
'subnet': len(subnets), "port": len(ports),
'secuirty_group': len(security_groups), "subnet": len(subnets),
'security_group_rule': len(security_group_rules) "secuirty_group": len(security_groups),
} "security_group_rule": len(security_group_rules),
}
def get_quota(self): 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): class ServiceQuotaOctaviaHelper(ServiceQuotaNeutronHelper):
def __init__(self, region_name, project_id): def __init__(self, region_name, project_id):
self.client = openstack_clients.get_octaviaclient( self.client = openstack_clients.get_octaviaclient(region=region_name)
region=region_name)
self.project_id = project_id self.project_id = project_id
def get_quota(self): def get_quota(self):
project_quota = self.client.quota_show( project_quota = self.client.quota_show(project_id=self.project_id)
project_id=self.project_id)
# NOTE(amelia): Instead of returning the default quota if ANY # NOTE(amelia): Instead of returning the default quota if ANY
# of the quotas are the default, the endpoint # of the quotas are the default, the endpoint
@ -137,40 +132,45 @@ class QuotaManager(object):
for name, quota in project_quota.items(): for name, quota in project_quota.items():
if quota is None: if quota is None:
if not default_quota: if not default_quota:
default_quota = self.client.quota_defaults_show()[ default_quota = self.client.quota_defaults_show()["quota"]
'quota']
project_quota[name] = default_quota[name] project_quota[name] = default_quota[name]
return project_quota return project_quota
def set_quota(self, values): 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): def get_usage(self):
usage = {} usage = {}
usage['load_balancer'] = len(self.client.load_balancer_list( usage["load_balancer"] = len(
project_id=self.project_id)['loadbalancers']) self.client.load_balancer_list(project_id=self.project_id)[
usage['listener'] = len(self.client.listener_list( "loadbalancers"
project_id=self.project_id)['listeners']) ]
)
usage["listener"] = len(
self.client.listener_list(project_id=self.project_id)["listeners"]
)
pools = self.client.pool_list( pools = self.client.pool_list(project_id=self.project_id)["pools"]
project_id=self.project_id)['pools'] usage["pool"] = len(pools)
usage['pool'] = len(pools)
members = [] members = []
for pool in pools: for pool in pools:
members += pool['members'] members += pool["members"]
usage['member'] = len(members) usage["member"] = len(members)
usage['health_monitor'] = len(self.client.health_monitor_list( usage["health_monitor"] = len(
project_id=self.project_id)['healthmonitors']) self.client.health_monitor_list(project_id=self.project_id)[
"healthmonitors"
]
)
return usage return usage
_quota_updaters = { _quota_updaters = {
'cinder': ServiceQuotaCinderHelper, "cinder": ServiceQuotaCinderHelper,
'nova': ServiceQuotaNovaHelper, "nova": ServiceQuotaNovaHelper,
'neutron': ServiceQuotaNeutronHelper, "neutron": ServiceQuotaNeutronHelper,
'octavia': ServiceQuotaOctaviaHelper, "octavia": ServiceQuotaOctaviaHelper,
} }
def __init__(self, project_id, size_difference_threshold=None): def __init__(self, project_id, size_difference_threshold=None):
@ -182,24 +182,23 @@ class QuotaManager(object):
quota_services = dict(CONF.quota.services) quota_services = dict(CONF.quota.services)
all_regions = quota_services.pop('*', None) all_regions = quota_services.pop("*", None)
if all_regions: if all_regions:
self.default_helpers = {} self.default_helpers = {}
for service in all_regions: for service in all_regions:
if service in self._quota_updaters: if service in self._quota_updaters:
self.default_helpers[service] = \ self.default_helpers[service] = self._quota_updaters[service]
self._quota_updaters[service]
for region, services in quota_services.items(): for region, services in quota_services.items():
self.helpers[region] = {} self.helpers[region] = {}
for service in services: for service in services:
if service in self._quota_updaters: if service in self._quota_updaters:
self.helpers[region][service] = \ self.helpers[region][service] = self._quota_updaters[service]
self._quota_updaters[service]
self.project_id = project_id self.project_id = project_id
self.size_diff_threshold = (size_difference_threshold self.size_diff_threshold = (
or self.default_size_diff_threshold) size_difference_threshold or self.default_size_diff_threshold
)
def get_current_region_quota(self, region_id): def get_current_region_quota(self, region_id):
current_quota = {} current_quota = {}
@ -239,7 +238,8 @@ class QuotaManager(object):
match_percentages.append(0.0) match_percentages.append(0.0)
# Calculate the average of how much it matches the setting # Calculate the average of how much it matches the setting
difference = abs( difference = abs(
(sum(match_percentages) / float(len(match_percentages))) - 1) (sum(match_percentages) / float(len(match_percentages))) - 1
)
quota_differences[size] = difference quota_differences[size] = difference
@ -253,15 +253,14 @@ class QuotaManager(object):
quota_differences_pruned = {} quota_differences_pruned = {}
for size, difference in quota_differences.items(): for size, difference in quota_differences.items():
if (difference <= diff_threshold): if difference <= diff_threshold:
quota_differences_pruned[size] = difference quota_differences_pruned[size] = difference
if len(quota_differences_pruned) > 0: if len(quota_differences_pruned) > 0:
return min( return min(quota_differences_pruned, key=quota_differences_pruned.get)
quota_differences_pruned, key=quota_differences_pruned.get)
# If we don't get a match return custom which means the project will # If we don't get a match return custom which means the project will
# need admin approval for any change # need admin approval for any change
return 'custom' return "custom"
def get_quota_change_options(self, quota_size): def get_quota_change_options(self, quota_size):
""" Get's the pre-approved quota change options for a given 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) change_options = self.get_quota_change_options(current_quota_size)
region_data = { region_data = {
'region': region_id, "region": region_id,
"current_quota": current_quota, "current_quota": current_quota,
"current_quota_size": current_quota_size, "current_quota_size": current_quota_size,
"quota_change_options": change_options, "quota_change_options": change_options,
} }
if include_usage: 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 return region_data
@ -317,11 +316,11 @@ class QuotaManager(object):
def set_region_quota(self, region_id, quota_dict): def set_region_quota(self, region_id, quota_dict):
notes = [] notes = []
for service_name, values in quota_dict.items(): for service_name, values in quota_dict.items():
updater_class = self.helpers.get( updater_class = self.helpers.get(region_id, self.default_helpers).get(
region_id, self.default_helpers).get(service_name) service_name
)
if not updater_class: if not updater_class:
notes.append("No quota updater found for %s. Ignoring" % notes.append("No quota updater found for %s. Ignoring" % service_name)
service_name)
continue continue
service_helper = updater_class(region_id, self.project_id) service_helper = updater_class(region_id, self.project_id)

View File

@ -27,10 +27,16 @@ octavia_cache = {}
class FakeProject(object): class FakeProject(object):
def __init__(
def __init__(self, name, description="", self,
domain_id='default', parent_id=None, name,
enabled=True, is_domain=False, **kwargs): description="",
domain_id="default",
parent_id=None,
enabled=True,
is_domain=False,
**kwargs,
):
self.id = uuid4().hex self.id = uuid4().hex
self.name = name self.name = name
self.description = description self.description = description
@ -45,10 +51,15 @@ class FakeProject(object):
class FakeUser(object): class FakeUser(object):
def __init__(
def __init__(self, name, password="123", domain_id='default', self,
enabled=True, default_project_id=None, name,
**kwargs): password="123",
domain_id="default",
enabled=True,
default_project_id=None,
**kwargs,
):
self.id = uuid4().hex self.id = uuid4().hex
self.name = name self.name = name
self.password = password self.password = password
@ -62,14 +73,12 @@ class FakeUser(object):
class FakeRole(object): class FakeRole(object):
def __init__(self, name): def __init__(self, name):
self.id = uuid4().hex self.id = uuid4().hex
self.name = name self.name = name
class FakeCredential(object): class FakeCredential(object):
def __init__(self, user_id, cred_type, blob, project_id=None): def __init__(self, user_id, cred_type, blob, project_id=None):
self.id = uuid4().hex self.id = uuid4().hex
self.user_id = user_id self.user_id = user_id
@ -79,27 +88,28 @@ class FakeCredential(object):
class FakeRoleAssignment(object): class FakeRoleAssignment(object):
def __init__(
def __init__(self, scope, role=None, role_name=None, user=None, self, scope, role=None, role_name=None, user=None, group=None, inherited=False
group=None, inherited=False): ):
if role: if role:
self.role = role self.role = role
elif role_name: elif role_name:
self.role = {'name': role_name} self.role = {"name": role_name}
else: else:
raise AttributeError("must supply 'role' or 'role_name'.") raise AttributeError("must supply 'role' or 'role_name'.")
self.scope = scope self.scope = scope
self.user = user self.user = user
self.group = group self.group = group
if inherited: if inherited:
self.scope['OS-INHERIT:inherited_to'] = "projects" self.scope["OS-INHERIT:inherited_to"] = "projects"
def __eq__(self, other): def __eq__(self, other):
return self.__dict__ == other.__dict__ return self.__dict__ == other.__dict__
def setup_identity_cache(projects=None, users=None, role_assignments=None, def setup_identity_cache(
credentials=None, extra_roles=None): projects=None, users=None, role_assignments=None, credentials=None, extra_roles=None
):
if extra_roles is None: if extra_roles is None:
extra_roles = [] extra_roles = []
if not projects: if not projects:
@ -111,15 +121,17 @@ def setup_identity_cache(projects=None, users=None, role_assignments=None,
if not credentials: if not credentials:
credentials = [] credentials = []
default_domain = FakeProject( default_domain = FakeProject(name="Default", is_domain=True)
name="Default", is_domain=True) default_domain.id = "default"
default_domain.id = 'default'
projects.append(default_domain) projects.append(default_domain)
admin_user = FakeUser( admin_user = FakeUser(
name="admin", password="password", email="admin@example.com", name="admin",
domain_id=default_domain.id) password="password",
email="admin@example.com",
domain_id=default_domain.id,
)
users.append(admin_user) users.append(admin_user)
@ -132,34 +144,28 @@ def setup_identity_cache(projects=None, users=None, role_assignments=None,
] + extra_roles ] + extra_roles
region_one = mock.Mock() region_one = mock.Mock()
region_one.id = 'RegionOne' region_one.id = "RegionOne"
region_two = mock.Mock() region_two = mock.Mock()
region_two.id = 'RegionTwo' region_two.id = "RegionTwo"
global identity_cache global identity_cache
identity_cache = { identity_cache = {
'users': {u.id: u for u in users}, "users": {u.id: u for u in users},
'new_users': [], "new_users": [],
'projects': {p.id: p for p in projects}, "projects": {p.id: p for p in projects},
'new_projects': [], "new_projects": [],
'role_assignments': role_assignments, "role_assignments": role_assignments,
'new_role_assignments': [], "new_role_assignments": [],
'roles': {r.id: r for r in roles}, "roles": {r.id: r for r in roles},
'regions': { "regions": {"RegionOne": region_one, "RegionTwo": region_two},
'RegionOne': region_one, "domains": {default_domain.id: default_domain,},
'RegionTwo': region_two "credentials": credentials,
},
'domains': {
default_domain.id: default_domain,
},
'credentials': credentials,
} }
class FakeManager(object): class FakeManager(object):
def __init__(self): def __init__(self):
# TODO(adriant): decide if we want to have some function calls # TODO(adriant): decide if we want to have some function calls
# throw errors if this is false. # throw errors if this is false.
@ -192,34 +198,33 @@ class FakeManager(object):
def find_user(self, name, domain): def find_user(self, name, domain):
domain = self._domain_from_id(domain) domain = self._domain_from_id(domain)
global identity_cache global identity_cache
for user in identity_cache['users'].values(): for user in identity_cache["users"].values():
if (user.name.lower() == name.lower() if user.name.lower() == name.lower() and user.domain_id == domain.id:
and user.domain_id == domain.id):
return user return user
return None return None
def get_user(self, user_id): def get_user(self, user_id):
global identity_cache global identity_cache
return identity_cache['users'].get(user_id, None) return identity_cache["users"].get(user_id, None)
def list_users(self, project): def list_users(self, project):
project = self._project_from_id(project) project = self._project_from_id(project)
global identity_cache global identity_cache
users = {} users = {}
for assignment in identity_cache['role_assignments']: for assignment in identity_cache["role_assignments"]:
if assignment.scope['project']['id'] == project.id: if assignment.scope["project"]["id"] == project.id:
user = users.get(assignment.user['id']) user = users.get(assignment.user["id"])
if not user: if not user:
user = self.get_user(assignment.user['id']) user = self.get_user(assignment.user["id"])
user.roles = [] user.roles = []
user.inherited_roles = [] user.inherited_roles = []
users[user.id] = user 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) user.inherited_roles.append(r)
else: else:
user.roles.append(r) user.roles.append(r)
@ -233,34 +238,39 @@ class FakeManager(object):
while project.parent_id: while project.parent_id:
project = self._project_from_id(project.parent_id) project = self._project_from_id(project.parent_id)
for assignment in identity_cache['role_assignments']: for assignment in identity_cache["role_assignments"]:
if assignment.scope['project']['id'] == project.id: if assignment.scope["project"]["id"] == project.id:
if not assignment.scope.get('OS-INHERIT:inherited_to'): if not assignment.scope.get("OS-INHERIT:inherited_to"):
continue continue
user = users.get(assignment.user['id']) user = users.get(assignment.user["id"])
if not user: if not user:
user = self.get_user(assignment.user['id']) user = self.get_user(assignment.user["id"])
user.roles = [] user.roles = []
user.inherited_roles = [] user.inherited_roles = []
users[user.id] = user users[user.id] = user
r = self.find_role(assignment.role['name']) r = self.find_role(assignment.role["name"])
user.roles.append(r) user.roles.append(r)
return users.values() return users.values()
def create_user(self, name, password, email, created_on, def create_user(
domain='default', default_project=None): self, name, password, email, created_on, domain="default", default_project=None
):
domain = self._domain_from_id(domain) domain = self._domain_from_id(domain)
default_project = self._project_from_id(default_project) default_project = self._project_from_id(default_project)
global identity_cache global identity_cache
user = FakeUser( user = FakeUser(
name=name, password=password, email=email, name=name,
domain_id=domain.id, default_project=default_project) password=password,
identity_cache['users'][user.id] = user email=email,
identity_cache['new_users'].append(user) domain_id=domain.id,
default_project=default_project,
)
identity_cache["users"][user.id] = user
identity_cache["new_users"].append(user)
return user return user
def update_user_password(self, user, password): def update_user_password(self, user, password):
@ -285,7 +295,7 @@ class FakeManager(object):
def find_role(self, name): def find_role(self, name):
global identity_cache global identity_cache
for role in identity_cache['roles'].values(): for role in identity_cache["roles"].values():
if role.name == name: if role.name == name:
return role return role
return None return None
@ -297,17 +307,20 @@ class FakeManager(object):
roles = [] roles = []
for assignment in identity_cache['role_assignments']: for assignment in identity_cache["role_assignments"]:
if (assignment.user['id'] == user.id if (
and assignment.scope['project']['id'] == project.id): assignment.user["id"] == user.id
and assignment.scope["project"]["id"] == project.id
):
if (assignment.scope.get('OS-INHERIT:inherited_to') and not if (
inherited) or ( assignment.scope.get("OS-INHERIT:inherited_to") and not inherited
inherited and not ) or (
assignment.scope.get('OS-INHERIT:inherited_to')): inherited and not assignment.scope.get("OS-INHERIT:inherited_to")
):
continue continue
r = self.find_role(assignment.role['name']) r = self.find_role(assignment.role["name"])
roles.append(r) roles.append(r)
return roles return roles
@ -319,25 +332,21 @@ class FakeManager(object):
user = self._user_from_id(user) user = self._user_from_id(user)
global identity_cache global identity_cache
projects = {} projects = {}
for assignment in identity_cache['role_assignments']: for assignment in identity_cache["role_assignments"]:
if assignment.user['id'] == user.id: if assignment.user["id"] == user.id:
r = self.find_role(assignment.role['name']) r = self.find_role(assignment.role["name"])
try: try:
projects[assignment.scope['project']['id']].append(r) projects[assignment.scope["project"]["id"]].append(r)
except KeyError: except KeyError:
projects[assignment.scope['project']['id']] = [r] projects[assignment.scope["project"]["id"]] = [r]
return projects return projects
def _make_role_assignment(self, user, role, project, inherited=False): def _make_role_assignment(self, user, role, project, inherited=False):
scope = { scope = {"project": {"id": project.id}}
'project': {
'id': project.id}}
if inherited: if inherited:
scope['OS-INHERIT:inherited_to'] = "projects" scope["OS-INHERIT:inherited_to"] = "projects"
role_assignment = FakeRoleAssignment( role_assignment = FakeRoleAssignment(
scope=scope, scope=scope, role={"name": role.name}, user={"id": user.id},
role={"name": role.name},
user={'id': user.id},
) )
return role_assignment return role_assignment
@ -350,45 +359,51 @@ class FakeManager(object):
global identity_cache global identity_cache
if role_assignment not in identity_cache['role_assignments']: if role_assignment not in identity_cache["role_assignments"]:
identity_cache['role_assignments'].append(role_assignment) identity_cache["role_assignments"].append(role_assignment)
identity_cache['new_role_assignments'].append(role_assignment) identity_cache["new_role_assignments"].append(role_assignment)
def remove_user_role(self, user, role, project, inherited=False): def remove_user_role(self, user, role, project, inherited=False):
user = self._user_from_id(user) user = self._user_from_id(user)
role = self._role_from_id(role) role = self._role_from_id(role)
project = self._project_from_id(project) project = self._project_from_id(project)
role_assignment = self._make_role_assignment(user, role, project, role_assignment = self._make_role_assignment(
inherited=inherited) user, role, project, inherited=inherited
)
global identity_cache global identity_cache
if role_assignment in identity_cache['role_assignments']: if role_assignment in identity_cache["role_assignments"]:
identity_cache['role_assignments'].remove(role_assignment) identity_cache["role_assignments"].remove(role_assignment)
def find_project(self, project_name, domain): def find_project(self, project_name, domain):
domain = self._domain_from_id(domain) domain = self._domain_from_id(domain)
global identity_cache global identity_cache
for project in identity_cache['projects'].values(): for project in identity_cache["projects"].values():
if (project.name.lower() == project_name.lower() if (
and project.domain_id == domain.id): project.name.lower() == project_name.lower()
and project.domain_id == domain.id
):
return project return project
return None return None
def get_project(self, project_id, subtree_as_ids=False, def get_project(self, project_id, subtree_as_ids=False, parents_as_ids=False):
parents_as_ids=False):
global identity_cache global identity_cache
project = identity_cache['projects'].get(project_id, None) project = identity_cache["projects"].get(project_id, None)
if subtree_as_ids: if subtree_as_ids:
subtree_list = [] subtree_list = []
prev_layer = [project.id, ] prev_layer = [
project.id,
]
current_layer = True current_layer = True
while current_layer: while current_layer:
current_layer = [s_project.id for s_project in current_layer = [
identity_cache['projects'].values() s_project.id
if project.parent_id in prev_layer] for s_project in identity_cache["projects"].values()
if project.parent_id in prev_layer
]
prev_layer = current_layer prev_layer = current_layer
subtree_list.append(current_layer) subtree_list.append(current_layer)
@ -398,8 +413,8 @@ class FakeManager(object):
parent_list = [] parent_list = []
parent_id = project.parent_id parent_id = project.parent_id
parent_list.append(parent_id) parent_list.append(parent_id)
while 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_id = identity_cache["projects"].get(parent_id, None)
parent_list.append(parent_id) parent_list.append(parent_id)
project.parent_ids = parent_list project.parent_ids = parent_list
@ -408,20 +423,23 @@ class FakeManager(object):
return project return project
def create_project(self, project_name, created_on, parent=None, def create_project(
domain='default', description=""): self, project_name, created_on, parent=None, domain="default", description=""
):
parent = self._project_from_id(parent) parent = self._project_from_id(parent)
domain = self._domain_from_id(domain) domain = self._domain_from_id(domain)
global identity_cache global identity_cache
project = FakeProject( project = FakeProject(
name=project_name, created_on=created_on, description=description, name=project_name,
domain_id=domain.id created_on=created_on,
description=description,
domain_id=domain.id,
) )
if parent: if parent:
project.parent_id = parent.id project.parent_id = parent.id
identity_cache['projects'][project.id] = project identity_cache["projects"][project.id] = project
identity_cache['new_projects'].append(project) identity_cache["new_projects"].append(project)
return project return project
def update_project(self, project, **kwargs): def update_project(self, project, **kwargs):
@ -433,27 +451,27 @@ class FakeManager(object):
def find_domain(self, domain_name): def find_domain(self, domain_name):
global identity_cache global identity_cache
for domain in identity_cache['domains'].values(): for domain in identity_cache["domains"].values():
if domain.name.lower() == domain_name.lower(): if domain.name.lower() == domain_name.lower():
return domain return domain
return None return None
def get_domain(self, domain_id): def get_domain(self, domain_id):
global identity_cache 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): def get_region(self, region_id):
global identity_cache global identity_cache
return identity_cache['regions'].get(region_id, None) return identity_cache["regions"].get(region_id, None)
def list_regions(self): def list_regions(self):
global identity_cache global identity_cache
return identity_cache['regions'].values() return identity_cache["regions"].values()
def list_credentials(self, user_id, cred_type=None): def list_credentials(self, user_id, cred_type=None):
global identity_cache global identity_cache
found = [] found = []
for cred in identity_cache['credentials']: for cred in identity_cache["credentials"]:
if cred.user_id == user_id: if cred.user_id == user_id:
if cred_type and cred.type == cred_type: if cred_type and cred.type == cred_type:
found.append(cred) found.append(cred)
@ -465,21 +483,20 @@ class FakeManager(object):
global identity_cache global identity_cache
user = self._user_from_id(user) user = self._user_from_id(user)
project = self._project_from_id(project) project = self._project_from_id(project)
cred = FakeCredential( cred = FakeCredential(user_id=user.id, blob=blob, cred_type=cred_type)
user_id=user.id, blob=blob, cred_type=cred_type)
if project: if project:
cred.project_id = project.id cred.project_id = project.id
identity_cache['credentials'].append(cred) identity_cache["credentials"].append(cred)
return cred return cred
def clear_credential_type(self, user_id, cred_type): def clear_credential_type(self, user_id, cred_type):
global identity_cache global identity_cache
found = [] found = []
for cred in identity_cache['credentials']: for cred in identity_cache["credentials"]:
if cred.user_id == user_id and cred.type == cred_type: if cred.user_id == user_id and cred.type == cred_type:
found.append(cred) found.append(cred)
for cred in found: for cred in found:
identity_cache['credentials'].remove(cred) identity_cache["credentials"].remove(cred)
# TODO(adriant): Move this to a BaseIdentityManager class when # TODO(adriant): Move this to a BaseIdentityManager class when
# it exists. # it exists.
@ -499,9 +516,12 @@ class FakeManager(object):
return list(set(all_roles)) return list(set(all_roles))
# merge mapping lists to form a flat permitted roles list # merge mapping lists to form a flat permitted roles list
manageable_role_names = [mrole for role_name in user_roles manageable_role_names = [
if role_name in roles_mapping mrole
for mrole in roles_mapping[role_name]] for role_name in user_roles
if role_name in roles_mapping
for mrole in roles_mapping[role_name]
]
# a set has unique items # a set has unique items
manageable_role_names = set(manageable_role_names) manageable_role_names = set(manageable_role_names)
return manageable_role_names return manageable_role_names
@ -510,6 +530,7 @@ class FakeManager(object):
class FakeOpenstackClient(object): class FakeOpenstackClient(object):
class Quotas(object): class Quotas(object):
""" Stub class for testing quotas """ """ Stub class for testing quotas """
def __init__(self, service): def __init__(self, service):
self.service = service self.service = service
@ -518,7 +539,8 @@ class FakeOpenstackClient(object):
def get(self, project_id): def get(self, project_id):
return self.QuotaSet( return self.QuotaSet(
self.service._cache[self.service.region][project_id]['quota']) self.service._cache[self.service.region][project_id]["quota"]
)
class QuotaSet(object): class QuotaSet(object):
def __init__(self, data): def __init__(self, data):
@ -536,58 +558,66 @@ class FakeOpenstackClient(object):
if self.region not in self._cache: if self.region not in self._cache:
self._cache[self.region] = {} self._cache[self.region] = {}
if project_id not in self._cache[self.region]: if project_id not in self._cache[self.region]:
self._cache[self.region][project_id] = { self._cache[self.region][project_id] = {"quota": {}}
'quota': {} quota = self._cache[self.region][project_id]["quota"]
}
quota = self._cache[self.region][project_id]['quota']
quota.update(kwargs) quota.update(kwargs)
class FakeNeutronClient(object): class FakeNeutronClient(object):
def __init__(self, region): def __init__(self, region):
self.region = region self.region = region
def create_network(self, body): def create_network(self, body):
global neutron_cache global neutron_cache
project_id = body['network']['tenant_id'] project_id = body["network"]["tenant_id"]
net = {'network': {'id': 'net_id_%s' % neutron_cache['RegionOne']['i'], net = {
'body': body}} "network": {
net_id = net['network']['id'] "id": "net_id_%s" % neutron_cache["RegionOne"]["i"],
neutron_cache['RegionOne'][project_id]['networks'][net_id] = net "body": body,
neutron_cache['RegionOne']['i'] += 1 }
}
net_id = net["network"]["id"]
neutron_cache["RegionOne"][project_id]["networks"][net_id] = net
neutron_cache["RegionOne"]["i"] += 1
return net return net
def create_subnet(self, body): def create_subnet(self, body):
global neutron_cache global neutron_cache
project_id = body['subnet']['tenant_id'] project_id = body["subnet"]["tenant_id"]
subnet = {'subnet': {'id': 'subnet_id_%s' subnet = {
% neutron_cache['RegionOne']['i'], "subnet": {
'body': body}} "id": "subnet_id_%s" % neutron_cache["RegionOne"]["i"],
sub_id = subnet['subnet']['id'] "body": body,
neutron_cache['RegionOne'][project_id]['subnets'][sub_id] = subnet }
neutron_cache['RegionOne']['i'] += 1 }
sub_id = subnet["subnet"]["id"]
neutron_cache["RegionOne"][project_id]["subnets"][sub_id] = subnet
neutron_cache["RegionOne"]["i"] += 1
return subnet return subnet
def create_router(self, body): def create_router(self, body):
global neutron_cache global neutron_cache
project_id = body['router']['tenant_id'] project_id = body["router"]["tenant_id"]
router = {'router': {'id': 'router_id_%s' router = {
% neutron_cache['RegionOne']['i'], "router": {
'body': body}} "id": "router_id_%s" % neutron_cache["RegionOne"]["i"],
router_id = router['router']['id'] "body": body,
neutron_cache['RegionOne'][project_id]['routers'][router_id] = router }
neutron_cache['RegionOne']['i'] += 1 }
router_id = router["router"]["id"]
neutron_cache["RegionOne"][project_id]["routers"][router_id] = router
neutron_cache["RegionOne"]["i"] += 1
return router return router
def add_interface_router(self, router_id, body): def add_interface_router(self, router_id, body):
global neutron_cache global neutron_cache
port_id = "port_id_%s" % neutron_cache['RegionOne']['i'] port_id = "port_id_%s" % neutron_cache["RegionOne"]["i"]
neutron_cache['RegionOne']['i'] += 1 neutron_cache["RegionOne"]["i"] += 1
interface = { interface = {
'port_id': port_id, "port_id": port_id,
'id': router_id, "id": router_id,
'subnet_id': body['subnet_id']} "subnet_id": body["subnet_id"],
}
return interface return interface
def update_quota(self, project_id, body): def update_quota(self, project_id, body):
@ -597,14 +627,14 @@ class FakeNeutronClient(object):
if project_id not in neutron_cache[self.region]: if project_id not in neutron_cache[self.region]:
neutron_cache[self.region][project_id] = {} neutron_cache[self.region][project_id] = {}
if 'quota' not in neutron_cache[self.region][project_id]: if "quota" not in neutron_cache[self.region][project_id]:
neutron_cache[self.region][project_id]['quota'] = {} neutron_cache[self.region][project_id]["quota"] = {}
quota = neutron_cache[self.region][project_id]['quota'] quota = neutron_cache[self.region][project_id]["quota"]
quota.update(body['quota']) quota.update(body["quota"])
def show_quota(self, project_id): 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): def list_networks(self, tenant_id):
return neutron_cache[self.region][tenant_id] return neutron_cache[self.region][tenant_id]
@ -630,11 +660,13 @@ class FakeNeutronClient(object):
class FakeOctaviaClient(object): class FakeOctaviaClient(object):
# {name in client call: name in response} # {name in client call: name in response}
resource_dict = {'load_balancer': 'loadbalancers', resource_dict = {
'listener': 'listeners', "load_balancer": "loadbalancers",
'member': 'members', "listener": "listeners",
'pool': 'pools', "member": "members",
'health_monitor': 'healthmonitors'} "pool": "pools",
"health_monitor": "healthmonitors",
}
# NOTE(amelia): Using the current octavia client we will get back # NOTE(amelia): Using the current octavia client we will get back
# dicts for everything, rather than the resources the # dicts for everything, rather than the resources the
@ -651,15 +683,15 @@ class FakeOctaviaClient(object):
def quota_show(self, project_id): def quota_show(self, project_id):
self._ensure_project_exists(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: for item in self.resource_dict:
if item not in quota: if item not in quota:
quota[item] = None quota[item] = None
return {'quota': quota} return {"quota": quota}
def quota_set(self, project_id, json): def quota_set(self, project_id, json):
self._ensure_project_exists(project_id) 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): def quota_defaults_show(self):
return { return {
@ -668,7 +700,7 @@ class FakeOctaviaClient(object):
"listener": -1, "listener": -1,
"member": 50, "member": 50,
"pool": -1, "pool": -1,
"health_monitor": -1 "health_monitor": -1,
} }
} }
@ -676,29 +708,27 @@ class FakeOctaviaClient(object):
def action(project_id=None): def action(project_id=None):
self._ensure_project_exists(project_id) self._ensure_project_exists(project_id)
resource = self.cache.get(project_id, {}).get(resource_type, []) 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] resource_name = self.resource_dict[resource_type]
return {resource_name: resource, links_name: []} return {resource_name: resource, links_name: []}
return action return action
def _ensure_project_exists(self, project_id): def _ensure_project_exists(self, project_id):
if project_id not in self.cache: if project_id not in self.cache:
self.cache[project_id] = { self.cache[project_id] = {name: [] for name in self.resource_dict.keys()}
name: [] for name in self.resource_dict.keys()} self.cache[project_id]["quota"] = dict(CONF.quota.sizes["small"]["octavia"])
self.cache[project_id]['quota'] = dict(
CONF.quota.sizes['small']['octavia'])
def __getattr__(self, name): def __getattr__(self, name):
# NOTE(amelia): This is out of pure laziness # NOTE(amelia): This is out of pure laziness
global octavia_cache 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]) return self.lister(name[:-5])
else: else:
raise AttributeError raise AttributeError
class FakeNovaClient(FakeOpenstackClient): class FakeNovaClient(FakeOpenstackClient):
def __init__(self, region): def __init__(self, region):
global nova_cache global nova_cache
super(FakeNovaClient, self).__init__(region, nova_cache) super(FakeNovaClient, self).__init__(region, nova_cache)
@ -730,7 +760,7 @@ class FakeCinderClient(FakeOpenstackClient):
def list(self, search_opts=None): def list(self, search_opts=None):
if search_opts: if search_opts:
project_id = search_opts['project_id'] project_id = search_opts["project_id"]
global cinder_cache global cinder_cache
return cinder_cache[self.region][project_id][self.key] return cinder_cache[self.region][project_id][self.key]
@ -739,9 +769,8 @@ class FakeCinderClient(FakeOpenstackClient):
self.region = region self.region = region
self._cache = cinder_cache self._cache = cinder_cache
self.quotas = FakeOpenstackClient.Quotas(self) self.quotas = FakeOpenstackClient.Quotas(self)
self.volumes = self.FakeResourceGroup(region, 'volumes') self.volumes = self.FakeResourceGroup(region, "volumes")
self.volume_snapshots = self.FakeResourceGroup(region, self.volume_snapshots = self.FakeResourceGroup(region, "volume_snapshots")
'volume_snapshots')
class FakeResource(object): class FakeResource(object):
@ -755,24 +784,25 @@ class FakeResource(object):
def setup_neutron_cache(region, project_id): def setup_neutron_cache(region, project_id):
global neutron_cache global neutron_cache
if region not in neutron_cache: if region not in neutron_cache:
neutron_cache[region] = {'i': 0} neutron_cache[region] = {"i": 0}
else: else:
neutron_cache[region]['i'] = 0 neutron_cache[region]["i"] = 0
if project_id not in neutron_cache[region]: if project_id not in neutron_cache[region]:
neutron_cache[region][project_id] = {} neutron_cache[region][project_id] = {}
neutron_cache[region][project_id] = { neutron_cache[region][project_id] = {
'networks': {}, "networks": {},
'subnets': {}, "subnets": {},
'routers': {}, "routers": {},
'security_groups': {}, "security_groups": {},
'floatingips': {}, "floatingips": {},
'security_group_rules': {}, "security_group_rules": {},
'ports': {}, "ports": {},
} }
neutron_cache[region][project_id]['quota'] = dict( neutron_cache[region][project_id]["quota"] = dict(
CONF.quota.sizes['small']['neutron']) CONF.quota.sizes["small"]["neutron"]
)
def setup_cinder_cache(region, project_id): 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] = {}
cinder_cache[region][project_id] = { cinder_cache[region][project_id] = {
'volumes': [], "volumes": [],
'volume_snapshots': [], "volume_snapshots": [],
} }
cinder_cache[region][project_id]['quota'] = dict( cinder_cache[region][project_id]["quota"] = dict(
CONF.quota.sizes['small']['cinder']) CONF.quota.sizes["small"]["cinder"]
)
def setup_nova_cache(region, project_id): def setup_nova_cache(region, project_id):
@ -800,19 +831,18 @@ def setup_nova_cache(region, project_id):
# Mocking the nova limits api # Mocking the nova limits api
nova_cache[region][project_id] = { nova_cache[region][project_id] = {
'absolute': { "absolute": {
"totalInstancesUsed": 0, "totalInstancesUsed": 0,
"totalFloatingIpsUsed": 0, "totalFloatingIpsUsed": 0,
"totalRAMUsed": 0, "totalRAMUsed": 0,
"totalCoresUsed": 0, "totalCoresUsed": 0,
"totalSecurityGroupsUsed": 0 "totalSecurityGroupsUsed": 0,
} }
} }
nova_cache[region][project_id]['quota'] = dict( nova_cache[region][project_id]["quota"] = dict(CONF.quota.sizes["small"]["nova"])
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 """ """ Sets up the quota cache for a given region and project """
global cinder_cache global cinder_cache
@ -820,36 +850,31 @@ def setup_quota_cache(region_name, project_id, size='small'):
cinder_cache[region_name] = {} cinder_cache[region_name] = {}
if project_id not in cinder_cache[region_name]: if project_id not in cinder_cache[region_name]:
cinder_cache[region_name][project_id] = { cinder_cache[region_name][project_id] = {"quota": {}}
'quota': {}
}
cinder_cache[region_name][project_id]['quota'] = dict( cinder_cache[region_name][project_id]["quota"] = dict(
CONF.quota.sizes[size]['cinder']) CONF.quota.sizes[size]["cinder"]
)
global nova_cache global nova_cache
if region_name not in nova_cache: if region_name not in nova_cache:
nova_cache[region_name] = {} nova_cache[region_name] = {}
if project_id not in nova_cache[region_name]: if project_id not in nova_cache[region_name]:
nova_cache[region_name][project_id] = { nova_cache[region_name][project_id] = {"quota": {}}
'quota': {}
}
nova_cache[region_name][project_id]['quota'] = dict( nova_cache[region_name][project_id]["quota"] = dict(CONF.quota.sizes[size]["nova"])
CONF.quota.sizes[size]['nova'])
global neutron_cache global neutron_cache
if region_name not in neutron_cache: if region_name not in neutron_cache:
neutron_cache[region_name] = {} neutron_cache[region_name] = {}
if project_id not in neutron_cache[region_name]: if project_id not in neutron_cache[region_name]:
neutron_cache[region_name][project_id] = { neutron_cache[region_name][project_id] = {"quota": {}}
'quota': {}
}
neutron_cache[region_name][project_id]['quota'] = dict( neutron_cache[region_name][project_id]["quota"] = dict(
CONF.quota.sizes[size]['neutron']) CONF.quota.sizes[size]["neutron"]
)
def setup_mock_caches(region, project_id): def setup_mock_caches(region, project_id):

View File

@ -19,7 +19,6 @@ from adjutant.common.tests import fake_clients
class AdjutantTestCase(TestCase): class AdjutantTestCase(TestCase):
def tearDown(self): def tearDown(self):
fake_clients.identity_cache.clear() fake_clients.identity_cache.clear()
fake_clients.neutron_cache.clear() fake_clients.neutron_cache.clear()
@ -28,7 +27,6 @@ class AdjutantTestCase(TestCase):
class AdjutantAPITestCase(APITestCase): class AdjutantAPITestCase(APITestCase):
def tearDown(self): def tearDown(self):
fake_clients.identity_cache.clear() fake_clients.identity_cache.clear()
fake_clients.neutron_cache.clear() fake_clients.neutron_cache.clear()

View File

@ -82,23 +82,20 @@ class IdentityManager(object): # pragma: no cover
users = {} users = {}
user_assignments = self.ks_client.role_assignments.list( user_assignments = self.ks_client.role_assignments.list(project=project)
project=project)
for assignment in user_assignments: for assignment in user_assignments:
try: try:
user = users.get(assignment.user['id'], None) user = users.get(assignment.user["id"], None)
if not user: if not user:
user = self.ks_client.users.get( user = self.ks_client.users.get(assignment.user["id"])
assignment.user['id'])
user.roles = [] user.roles = []
user.inherited_roles = [] user.inherited_roles = []
users[user.id] = user users[user.id] = user
if assignment.scope.get('OS-INHERIT:inherited_to'): if assignment.scope.get("OS-INHERIT:inherited_to"):
user.inherited_roles.append( user.inherited_roles.append(role_dict[assignment.role["id"]])
role_dict[assignment.role['id']])
else: else:
user.roles.append(role_dict[assignment.role['id']]) user.roles.append(role_dict[assignment.role["id"]])
except AttributeError: except AttributeError:
# Just means the assignment is a group, so ignore it. # Just means the assignment is a group, so ignore it.
pass pass
@ -119,22 +116,19 @@ class IdentityManager(object): # pragma: no cover
project = self.ks_client.projects.get(project) project = self.ks_client.projects.get(project)
while project.parent_id: while project.parent_id:
project = self.ks_client.projects.get(project.parent_id) project = self.ks_client.projects.get(project.parent_id)
user_assignments = self.ks_client.role_assignments.list( user_assignments = self.ks_client.role_assignments.list(project=project)
project=project)
for assignment in user_assignments: for assignment in user_assignments:
if not assignment.scope.get('OS-INHERIT:inherited_to'): if not assignment.scope.get("OS-INHERIT:inherited_to"):
continue continue
try: try:
user = users.get( user = users.get(assignment.user["id"], None)
assignment.user['id'], None)
if user: if user:
user.roles.append( user.roles.append(role_dict[assignment.role["id"]])
role_dict[assignment.role['id']])
else: else:
user = self.ks_client.users.get( user = self.ks_client.users.get(assignment.user["id"])
assignment.user['id'])
user.roles = [ user.roles = [
role_dict[assignment.role['id']], ] role_dict[assignment.role["id"]],
]
user.inherited_roles = [] user.inherited_roles = []
users[user.id] = user users[user.id] = user
except AttributeError: except AttributeError:
@ -146,12 +140,18 @@ class IdentityManager(object): # pragma: no cover
return [] return []
return users.values() return users.values()
def create_user(self, name, password, email, created_on, domain=None, def create_user(
default_project=None): self, name, password, email, created_on, domain=None, default_project=None
):
user = self.ks_client.users.create( user = self.ks_client.users.create(
name=name, password=password, domain=domain, email=email, name=name,
default_project=default_project, created_on=created_on) password=password,
domain=domain,
email=email,
default_project=default_project,
created_on=created_on,
)
return user return user
def enable_user(self, user): def enable_user(self, user):
@ -182,14 +182,14 @@ class IdentityManager(object): # pragma: no cover
user_roles = [] user_roles = []
user_assignments = self.ks_client.role_assignments.list( user_assignments = self.ks_client.role_assignments.list(
user=user, project=project) user=user, project=project
)
for assignment in user_assignments: for assignment in user_assignments:
if (assignment.scope.get('OS-INHERIT:inherited_to') and not if (assignment.scope.get("OS-INHERIT:inherited_to") and not inherited) or (
inherited) or ( inherited and not assignment.scope.get("OS-INHERIT:inherited_to")
inherited and not ):
assignment.scope.get('OS-INHERIT:inherited_to')):
continue continue
user_roles.append(role_dict[assignment.role['id']]) user_roles.append(role_dict[assignment.role["id"]])
return user_roles return user_roles
def get_all_roles(self, user): 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) user_assignments = self.ks_client.role_assignments.list(user=user)
projects = defaultdict(list) projects = defaultdict(list)
for assignment in user_assignments: for assignment in user_assignments:
project = assignment.scope['project']['id'] project = assignment.scope["project"]["id"]
projects[project].append(role_dict[assignment.role['id']]) projects[project].append(role_dict[assignment.role["id"]])
return projects return projects
@ -213,8 +213,11 @@ class IdentityManager(object): # pragma: no cover
try: try:
if inherited: if inherited:
self.ks_client.roles.grant( self.ks_client.roles.grant(
role, user=user, project=project, role,
os_inherit_extension_inherited=inherited) user=user,
project=project,
os_inherit_extension_inherited=inherited,
)
else: else:
self.ks_client.roles.grant(role, user=user, project=project) self.ks_client.roles.grant(role, user=user, project=project)
except ks_exceptions.Conflict: except ks_exceptions.Conflict:
@ -224,8 +227,11 @@ class IdentityManager(object): # pragma: no cover
def remove_user_role(self, user, role, project, inherited=False): def remove_user_role(self, user, role, project, inherited=False):
if inherited: if inherited:
self.ks_client.roles.revoke( self.ks_client.roles.revoke(
role, user=user, project=project, role,
os_inherit_extension_inherited=inherited) user=user,
project=project,
os_inherit_extension_inherited=inherited,
)
else: else:
self.ks_client.roles.revoke(role, user=user, project=project) self.ks_client.roles.revoke(role, user=user, project=project)
@ -233,8 +239,7 @@ class IdentityManager(object): # pragma: no cover
try: try:
# Using a filtered list as find is more efficient than # Using a filtered list as find is more efficient than
# using the client find # using the client find
projects = self.ks_client.projects.list( projects = self.ks_client.projects.list(name=project_name, domain=domain)
name=project_name, domain=domain)
if projects: if projects:
# NOTE(adriant) project names are unique in a domain so # NOTE(adriant) project names are unique in a domain so
# it is safe to assume filtering on project name and domain # 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: except ks_exceptions.NotFound:
return None return None
def get_project(self, project_id, subtree_as_ids=False, def get_project(self, project_id, subtree_as_ids=False, parents_as_ids=False):
parents_as_ids=False):
try: try:
project = self.ks_client.projects.get( project = self.ks_client.projects.get(
project_id, subtree_as_ids=subtree_as_ids, project_id, subtree_as_ids=subtree_as_ids, parents_as_ids=parents_as_ids
parents_as_ids=parents_as_ids) )
if parents_as_ids: if parents_as_ids:
depth = 1 depth = 1
last_root = None last_root = None
@ -276,21 +280,31 @@ class IdentityManager(object): # pragma: no cover
except ks_exceptions.NotFound: except ks_exceptions.NotFound:
return [] return []
def update_project(self, project, name=None, domain=None, description=None, def update_project(
enabled=None, **kwargs): self, project, name=None, domain=None, description=None, enabled=None, **kwargs
):
try: try:
return self.ks_client.projects.update( return self.ks_client.projects.update(
project=project, domain=domain, name=name, project=project,
description=description, enabled=enabled, domain=domain,
**kwargs) name=name,
description=description,
enabled=enabled,
**kwargs,
)
except ks_exceptions.NotFound: except ks_exceptions.NotFound:
return None return None
def create_project(self, project_name, created_on, parent=None, def create_project(
domain=None, description=""): self, project_name, created_on, parent=None, domain=None, description=""
):
project = self.ks_client.projects.create( project = self.ks_client.projects.create(
project_name, domain, parent=parent, created_on=created_on, project_name,
description=description) domain,
parent=parent,
created_on=created_on,
description=description,
)
return project return project
def get_domain(self, domain_id): def get_domain(self, domain_id):
@ -321,20 +335,19 @@ class IdentityManager(object): # pragma: no cover
return self.ks_client.regions.list(**kwargs) return self.ks_client.regions.list(**kwargs)
def list_credentials(self, user_id, cred_type=None): def list_credentials(self, user_id, cred_type=None):
return self.ks_client.credentials.list( return self.ks_client.credentials.list(user_id=user_id, type=cred_type)
user_id=user_id, type=cred_type)
def add_credential(self, user, cred_type, blob, project=None): def add_credential(self, user, cred_type, blob, project=None):
return self.ks_client.credentials.create( 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): def delete_credential(self, credential):
return self.ks_client.credentials.delete(credential) return self.ks_client.credentials.delete(credential)
def clear_credential_type(self, user_id, cred_type): def clear_credential_type(self, user_id, cred_type):
# list credentials of the type for the user # list credentials of the type for the user
credentials = self.ks_client.credentials.list( credentials = self.ks_client.credentials.list(user_id=user_id, type=cred_type)
user_id=user_id, type=cred_type)
for cred in credentials: for cred in credentials:
if cred.user_id == user_id and cred.type == cred_type: if cred.user_id == user_id and cred.type == cred_type:
self.ks_client.credentials.delete(cred) self.ks_client.credentials.delete(cred)
@ -357,9 +370,12 @@ class IdentityManager(object): # pragma: no cover
return list(set(all_roles)) return list(set(all_roles))
# merge mapping lists to form a flat permitted roles list # merge mapping lists to form a flat permitted roles list
manageable_role_names = [mrole for role_name in user_roles manageable_role_names = [
if role_name in roles_mapping mrole
for mrole in roles_mapping[role_name]] for role_name in user_roles
if role_name in roles_mapping
for mrole in roles_mapping[role_name]
]
# a set has unique items # a set has unique items
manageable_role_names = set(manageable_role_names) manageable_role_names = set(manageable_role_names)
return manageable_role_names return manageable_role_names

View File

@ -40,13 +40,13 @@ _old_config_file = "/etc/adjutant/conf.yaml"
_test_mode_commands = [ _test_mode_commands = [
# Adjutant commands: # Adjutant commands:
'exampleconfig', "exampleconfig",
# Django commands: # Django commands:
'check', "check",
'makemigrations', "makemigrations",
'squashmigrations', "squashmigrations",
'test', "test",
'testserver', "testserver",
] ]
@ -81,7 +81,11 @@ def _load_config():
% conf_file_loc % 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( print(
"DEPRECATED: Using the old default config location '%s' is deprecated " "DEPRECATED: Using the old default config location '%s' is deprecated "
"in favor of '%s', or setting a config location via the environment " "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.", help_text="List of Active Delegate APIs.",
required=True, required=True,
default=[ default=[
'UserRoles', "UserRoles",
'UserDetail', "UserDetail",
'UserResetPassword', "UserResetPassword",
'UserList', "UserList",
'RoleList', "RoleList",
], ],
# NOTE(adriant): for testing purposes we include ALL default APIs # NOTE(adriant): for testing purposes we include ALL default APIs
test_default=[ test_default=[
'UserRoles', "UserRoles",
'UserDetail', "UserDetail",
'UserResetPassword', "UserResetPassword",
'UserList', "UserList",
'RoleList', "RoleList",
'SignUp', "SignUp",
'UpdateProjectQuotas', "UpdateProjectQuotas",
'CreateProjectAndUser', "CreateProjectAndUser",
'InviteUser', "InviteUser",
'ResetPassword', "ResetPassword",
'EditUser', "EditUser",
'UpdateEmail', "UpdateEmail",
], ],
) )
) )

View File

@ -49,7 +49,7 @@ config_group.register_child_config(
fields.StrConfig( fields.StrConfig(
"secure_proxy_ssl_header", "secure_proxy_ssl_header",
help_text="The header representing a HTTP header/value combination " 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", default="HTTP_X_FORWARDED_PROTO",
) )
) )
@ -57,7 +57,7 @@ config_group.register_child_config(
fields.StrConfig( fields.StrConfig(
"secure_proxy_ssl_header_value", "secure_proxy_ssl_header_value",
help_text="The value representing a HTTP header/value combination " help_text="The value representing a HTTP header/value combination "
"that signifies a request is secure.", "that signifies a request is secure.",
default="https", default="https",
) )
) )
@ -83,7 +83,7 @@ config_group.register_child_config(
fields.StrConfig( fields.StrConfig(
"log_file", "log_file",
help_text="The name and location of the Adjutant 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", default="adjutant.log",
) )
) )

View File

@ -50,23 +50,14 @@ config_group.register_child_config(
check_value_type=True, check_value_type=True,
is_json=True, is_json=True,
default={ default={
'admin': [ "admin": ["project_admin", "project_mod", "heat_stack_owner", "member",],
'project_admin', "project_admin": [
'project_mod', "project_admin",
'heat_stack_owner', "project_mod",
'member', "heat_stack_owner",
], "member",
'project_admin': [
'project_admin',
'project_mod',
'heat_stack_owner',
'member',
],
'project_mod': [
'project_mod',
'heat_stack_owner',
'member',
], ],
"project_mod": ["project_mod", "heat_stack_owner", "member",],
}, },
test_default={ test_default={
"admin": ["project_admin", "project_mod", "member", "heat_stack_owner"], "admin": ["project_admin", "project_mod", "member", "heat_stack_owner"],

View File

@ -18,36 +18,32 @@ from confspirator import types
DEFAULT_QUOTA_SIZES = { DEFAULT_QUOTA_SIZES = {
'small': { "small": {
'nova': { "nova": {
'instances': 10, "instances": 10,
'cores': 20, "cores": 20,
'ram': 65536, "ram": 65536,
'floating_ips': 10, "floating_ips": 10,
'fixed_ips': 0, "fixed_ips": 0,
'metadata_items': 128, "metadata_items": 128,
'injected_files': 5, "injected_files": 5,
'injected_file_content_bytes': 10240, "injected_file_content_bytes": 10240,
'key_pairs': 50, "key_pairs": 50,
'security_groups': 20, "security_groups": 20,
'security_group_rules': 100, "security_group_rules": 100,
}, },
'cinder': { "cinder": {"gigabytes": 5000, "snapshots": 50, "volumes": 20,},
'gigabytes': 5000, "neutron": {
'snapshots': 50, "floatingip": 10,
'volumes': 20, "network": 3,
}, "port": 50,
'neutron': { "router": 3,
'floatingip': 10, "security_group": 20,
'network': 3, "security_group_rule": 100,
'port': 50, "subnet": 3,
'router': 3,
'security_group': 20,
'security_group_rule': 100,
'subnet': 3,
}, },
"octavia": { "octavia": {
'health_monitor': 5, "health_monitor": 5,
"listener": 1, "listener": 1,
"load_balancer": 1, "load_balancer": 1,
"member": 2, "member": 2,
@ -55,11 +51,7 @@ DEFAULT_QUOTA_SIZES = {
}, },
}, },
"medium": { "medium": {
"cinder": { "cinder": {"gigabytes": 10000, "volumes": 100, "snapshots": 300},
"gigabytes": 10000,
"volumes": 100,
"snapshots": 300
},
"nova": { "nova": {
"metadata_items": 128, "metadata_items": 128,
"injected_file_content_bytes": 10240, "injected_file_content_bytes": 10240,
@ -71,7 +63,7 @@ DEFAULT_QUOTA_SIZES = {
"injected_files": 5, "injected_files": 5,
"cores": 100, "cores": 100,
"fixed_ips": 0, "fixed_ips": 0,
"security_groups": 50 "security_groups": 50,
}, },
"neutron": { "neutron": {
"security_group_rule": 400, "security_group_rule": 400,
@ -80,10 +72,10 @@ DEFAULT_QUOTA_SIZES = {
"floatingip": 25, "floatingip": 25,
"security_group": 50, "security_group": 50,
"router": 5, "router": 5,
"port": 250 "port": 250,
}, },
"octavia": { "octavia": {
'health_monitor': 50, "health_monitor": 50,
"listener": 5, "listener": 5,
"load_balancer": 5, "load_balancer": 5,
"member": 5, "member": 5,
@ -91,11 +83,7 @@ DEFAULT_QUOTA_SIZES = {
}, },
}, },
"large": { "large": {
"cinder": { "cinder": {"gigabytes": 50000, "volumes": 200, "snapshots": 600},
"gigabytes": 50000,
"volumes": 200,
"snapshots": 600
},
"nova": { "nova": {
"metadata_items": 128, "metadata_items": 128,
"injected_file_content_bytes": 10240, "injected_file_content_bytes": 10240,
@ -107,7 +95,7 @@ DEFAULT_QUOTA_SIZES = {
"injected_files": 5, "injected_files": 5,
"cores": 200, "cores": 200,
"fixed_ips": 0, "fixed_ips": 0,
"security_groups": 100 "security_groups": 100,
}, },
"neutron": { "neutron": {
"security_group_rule": 800, "security_group_rule": 800,
@ -116,10 +104,10 @@ DEFAULT_QUOTA_SIZES = {
"floatingip": 50, "floatingip": 50,
"security_group": 100, "security_group": 100,
"router": 10, "router": 10,
"port": 500 "port": 500,
}, },
"octavia": { "octavia": {
'health_monitor': 100, "health_monitor": 100,
"listener": 10, "listener": 10,
"load_balancer": 10, "load_balancer": 10,
"member": 10, "member": 10,
@ -145,16 +133,16 @@ config_group.register_child_config(
fields.ListConfig( fields.ListConfig(
"sizes_ascending", "sizes_ascending",
help_text="An ascending list of all the quota size names, " help_text="An ascending list of all the quota size names, "
"so that Adjutant knows their relative sizes/order.", "so that Adjutant knows their relative sizes/order.",
default=['small', 'medium', 'large'], default=["small", "medium", "large"],
) )
) )
config_group.register_child_config( config_group.register_child_config(
fields.DictConfig( fields.DictConfig(
"services", "services",
help_text="A per region definition of what services Adjutant should manage " 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(), 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( fields.StrConfig(
"subject", "subject",
help_text="Default email subject for this stage", help_text="Default email subject for this stage",
default=email_subject) default=email_subject,
)
) )
email_group.register_child_config( email_group.register_child_config(
fields.StrConfig( fields.StrConfig(
"from", "from", help_text="Default from email for this stage", default=email_from
help_text="Default from email for this stage", )
default=email_from)
) )
email_group.register_child_config( email_group.register_child_config(
fields.StrConfig( fields.StrConfig(
"reply", "reply",
help_text="Default reply-to email for this stage", help_text="Default reply-to email for this stage",
default=email_reply) default=email_reply,
)
) )
email_group.register_child_config( email_group.register_child_config(
fields.StrConfig( fields.StrConfig(
"template", "template",
help_text="Default email template for this stage", help_text="Default email template for this stage",
default=email_template) default=email_template,
)
) )
email_group.register_child_config( email_group.register_child_config(
fields.StrConfig( fields.StrConfig(
"html_template", "html_template",
help_text="Default email html template for this stage", help_text="Default email html template for this stage",
default=email_html_template) default=email_html_template,
)
) )
return email_group return email_group
@ -123,9 +126,7 @@ _notifications_defaults_group.register_child_config(
"standard_handlers", "standard_handlers",
help_text="Handlers to use for standard notifications.", help_text="Handlers to use for standard notifications.",
required=True, required=True,
default=[ default=["EmailNotification",],
'EmailNotification',
],
) )
) )
_notifications_defaults_group.register_child_config( _notifications_defaults_group.register_child_config(
@ -133,9 +134,7 @@ _notifications_defaults_group.register_child_config(
"error_handlers", "error_handlers",
help_text="Handlers to use for error notifications.", help_text="Handlers to use for error notifications.",
required=True, required=True,
default=[ default=["EmailNotification",],
'EmailNotification',
],
) )
) )
_notifications_defaults_group.register_child_config( _notifications_defaults_group.register_child_config(
@ -159,7 +158,7 @@ _notifications_defaults_group.register_child_config(
"safe_errors", "safe_errors",
help_text="Error types which are safe to acknowledge automatically.", help_text="Error types which are safe to acknowledge automatically.",
required=True, required=True,
default=['SMTPException'], default=["SMTPException"],
) )
) )

View File

@ -36,28 +36,23 @@ class AdjutantCore(BaseFeatureSet):
project_actions.NewProjectWithUserAction, project_actions.NewProjectWithUserAction,
project_actions.NewProjectAction, project_actions.NewProjectAction,
project_actions.AddDefaultUsersToProjectAction, project_actions.AddDefaultUsersToProjectAction,
resource_actions.NewDefaultNetworkAction, resource_actions.NewDefaultNetworkAction,
resource_actions.NewProjectDefaultNetworkAction, resource_actions.NewProjectDefaultNetworkAction,
resource_actions.SetProjectQuotaAction, resource_actions.SetProjectQuotaAction,
resource_actions.UpdateProjectQuotasAction, resource_actions.UpdateProjectQuotasAction,
user_actions.NewUserAction, user_actions.NewUserAction,
user_actions.ResetUserPasswordAction, user_actions.ResetUserPasswordAction,
user_actions.EditUserRolesAction, user_actions.EditUserRolesAction,
user_actions.UpdateUserEmailAction, user_actions.UpdateUserEmailAction,
misc_actions.SendAdditionalEmailAction, misc_actions.SendAdditionalEmailAction,
] ]
tasks = [ tasks = [
project_tasks.CreateProjectAndUser, project_tasks.CreateProjectAndUser,
user_tasks.EditUserRoles, user_tasks.EditUserRoles,
user_tasks.InviteUser, user_tasks.InviteUser,
user_tasks.ResetUserPassword, user_tasks.ResetUserPassword,
user_tasks.UpdateUserEmail, user_tasks.UpdateUserEmail,
resource_tasks.UpdateProjectQuotas, resource_tasks.UpdateProjectQuotas,
] ]
@ -67,7 +62,6 @@ class AdjutantCore(BaseFeatureSet):
task_apis.ResetPassword, task_apis.ResetPassword,
task_apis.EditUser, task_apis.EditUser,
task_apis.UpdateEmail, task_apis.UpdateEmail,
openstack_apis.UserList, openstack_apis.UserList,
openstack_apis.UserDetail, openstack_apis.UserDetail,
openstack_apis.UserRoles, 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 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. to the user as an ServiceUnavailable error with a 503 response.
""" """
default_message = "A internal service error has occured." default_message = "A internal service error has occured."
def __init__(self, message=None): def __init__(self, message=None):
@ -34,28 +35,23 @@ class BaseServiceException(Exception):
class InvalidActionClass(BaseServiceException): class InvalidActionClass(BaseServiceException):
default_message = ( default_message = "Cannot register action not built off the BaseAction class."
"Cannot register action not built off the BaseAction class.")
class InvalidActionSerializer(BaseServiceException): class InvalidActionSerializer(BaseServiceException):
default_message = ( default_message = "Action serializer must be a valid DRF serializer."
"Action serializer must be a valid DRF serializer.")
class InvalidTaskClass(BaseServiceException): class InvalidTaskClass(BaseServiceException):
default_message = ( default_message = "Action serializer must be a valid DRF serializer."
"Action serializer must be a valid DRF serializer.")
class InvalidAPIClass(BaseServiceException): class InvalidAPIClass(BaseServiceException):
default_message = ( default_message = "Cannot register task not built off the BaseTask class."
"Cannot register task not built off the BaseTask class.")
class DelegateAPINotRegistered(BaseServiceException): class DelegateAPINotRegistered(BaseServiceException):
default_message = ( default_message = "Failed to setup DelegateAPI that has not been registered."
"Failed to setup DelegateAPI that has not been registered.")
class TaskNotRegistered(BaseServiceException): class TaskNotRegistered(BaseServiceException):
@ -76,6 +72,7 @@ class ConfigurationException(BaseServiceException):
class BaseAPIException(Exception): class BaseAPIException(Exception):
"""An Task error occurred.""" """An Task error occurred."""
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST
def __init__(self, message=None, internal_message=None): def __init__(self, message=None, internal_message=None):
@ -95,17 +92,17 @@ class BaseAPIException(Exception):
class NotFound(BaseAPIException): class NotFound(BaseAPIException):
status_code = status.HTTP_404_NOT_FOUND status_code = status.HTTP_404_NOT_FOUND
default_message = 'Not found.' default_message = "Not found."
class TaskNotFound(NotFound): class TaskNotFound(NotFound):
status_code = status.HTTP_404_NOT_FOUND status_code = status.HTTP_404_NOT_FOUND
default_message = 'Task not found.' default_message = "Task not found."
class ServiceUnavailable(BaseAPIException): class ServiceUnavailable(BaseAPIException):
status_code = status.HTTP_503_SERVICE_UNAVAILABLE 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): class TaskSerializersInvalid(BaseAPIException):
@ -145,5 +142,6 @@ class TaskStateInvalid(BaseTaskException):
class TaskActionsFailed(BaseTaskException): class TaskActionsFailed(BaseTaskException):
"""For use when Task processing fails and we want to wrap that.""" """For use when Task processing fails and we want to wrap that."""
status_code = status.HTTP_503_SERVICE_UNAVAILABLE 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): def register_action_class(action_class):
if not issubclass(action_class, BaseAction): if not issubclass(action_class, BaseAction):
raise exceptions.InvalidActionClass( raise exceptions.InvalidActionClass(
"'%s' is not a built off the BaseAction class." "'%s' is not a built off the BaseAction class." % action_class.__name__
% action_class.__name__
) )
if action_class.serializer and not issubclass( if action_class.serializer and not issubclass(
action_class.serializer, drf_serializers.Serializer): action_class.serializer, drf_serializers.Serializer
):
raise exceptions.InvalidActionSerializer( raise exceptions.InvalidActionSerializer(
"serializer for '%s' is not a valid DRF serializer." "serializer for '%s' is not a valid DRF serializer." % action_class.__name__
% action_class.__name__
) )
data = {} data = {}
data[action_class.__name__] = action_class 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 # NOTE(adriant): We copy the config_group before naming it
# to avoid cases where a subclass inherits but doesn't extend it # to avoid cases where a subclass inherits but doesn't extend it
setting_group = action_class.config_group.copy() setting_group = action_class.config_group.copy()
setting_group.set_name( setting_group.set_name(action_class.__name__, reformat_name=False)
action_class.__name__, reformat_name=False)
action_defaults_group.register_child_config(setting_group) action_defaults_group.register_child_config(setting_group)
def register_task_class(task_class): def register_task_class(task_class):
if not issubclass(task_class, tasks_base.BaseTask): if not issubclass(task_class, tasks_base.BaseTask):
raise exceptions.InvalidTaskClass( raise exceptions.InvalidTaskClass(
"'%s' is not a built off the BaseTask class." "'%s' is not a built off the BaseTask class." % task_class.__name__
% task_class.__name__
) )
data = {} data = {}
data[task_class.task_type] = task_class data[task_class.task_type] = task_class
@ -78,16 +75,14 @@ def register_task_class(task_class):
tasks.TASK_CLASSES.update(data) tasks.TASK_CLASSES.update(data)
config_group = tasks_base.make_task_config(task_class) config_group = tasks_base.make_task_config(task_class)
config_group.set_name( config_group.set_name(task_class.task_type, reformat_name=False)
task_class.task_type, reformat_name=False)
tasks_group.register_child_config(config_group) tasks_group.register_child_config(config_group)
def register_delegate_api_class(api_class): def register_delegate_api_class(api_class):
if not issubclass(api_class, BaseDelegateAPI): if not issubclass(api_class, BaseDelegateAPI):
raise exceptions.InvalidAPIClass( raise exceptions.InvalidAPIClass(
"'%s' is not a built off the BaseDelegateAPI class." "'%s' is not a built off the BaseDelegateAPI class." % api_class.__name__
% api_class.__name__
) )
data = {} data = {}
data[api_class.__name__] = api_class 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 # NOTE(adriant): We copy the config_group before naming it
# to avoid cases where a subclass inherits but doesn't extend it # to avoid cases where a subclass inherits but doesn't extend it
setting_group = api_class.config_group.copy() setting_group = api_class.config_group.copy()
setting_group.set_name( setting_group.set_name(api_class.__name__, reformat_name=False)
api_class.__name__, reformat_name=False)
api_config.register_child_config(setting_group) 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): def register_feature_set_config(feature_set_group):
if not isinstance(feature_set_group, groups.ConfigGroup): if not isinstance(feature_set_group, groups.ConfigGroup):
raise conf_exceptions.InvalidConfigClass( 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) feature_set_config.register_child_config(feature_set_group)
@ -149,7 +144,7 @@ class BaseFeatureSet(object):
config = None config = None
def __init__(self): def __init__(self):
self.logger = getLogger('adjutant') self.logger = getLogger("adjutant")
def load(self): def load(self):
self.logger.info("Loading feature set: '%s'" % self.__class__.__name__) 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 Middleware to build an easy to use dict of important data from
what the keystone wsgi middleware gives us. what the keystone wsgi middleware gives us.
""" """
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
def __call__(self, request): def __call__(self, request):
try: try:
token_data = { token_data = {
'project_domain_id': request.META['HTTP_X_PROJECT_DOMAIN_ID'], "project_domain_id": request.META["HTTP_X_PROJECT_DOMAIN_ID"],
'project_name': request.META['HTTP_X_PROJECT_NAME'], "project_name": request.META["HTTP_X_PROJECT_NAME"],
'project_id': request.META['HTTP_X_PROJECT_ID'], "project_id": request.META["HTTP_X_PROJECT_ID"],
'roles': request.META['HTTP_X_ROLES'].split(','), "roles": request.META["HTTP_X_ROLES"].split(","),
'user_domain_id': request.META['HTTP_X_USER_DOMAIN_ID'], "user_domain_id": request.META["HTTP_X_USER_DOMAIN_ID"],
'username': request.META['HTTP_X_USER_NAME'], "username": request.META["HTTP_X_USER_NAME"],
'user_id': request.META['HTTP_X_USER_ID'], "user_id": request.META["HTTP_X_USER_ID"],
'authenticated': request.META['HTTP_X_IDENTITY_STATUS'] "authenticated": request.META["HTTP_X_IDENTITY_STATUS"],
} }
except KeyError: except KeyError:
token_data = {} token_data = {}
@ -49,6 +50,7 @@ class TestingHeaderUnwrapper:
""" """
Replacement for the KeystoneHeaderUnwrapper for testing purposes. Replacement for the KeystoneHeaderUnwrapper for testing purposes.
""" """
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
@ -58,17 +60,18 @@ class TestingHeaderUnwrapper:
# TODO(adriant): follow up patch to update all the test # TODO(adriant): follow up patch to update all the test
# headers to provide domain values. # headers to provide domain values.
# Default here is just a temporary measure. # Default here is just a temporary measure.
'project_domain_id': "project_domain_id": request.META["headers"].get(
request.META['headers'].get( "project_domain_id", "default"
'project_domain_id', 'default'), ),
'project_name': request.META['headers']['project_name'], "project_name": request.META["headers"]["project_name"],
'project_id': request.META['headers']['project_id'], "project_id": request.META["headers"]["project_id"],
'roles': request.META['headers']['roles'].split(','), "roles": request.META["headers"]["roles"].split(","),
'user_domain_id': "user_domain_id": request.META["headers"].get(
request.META['headers'].get('user_domain_id', 'default'), "user_domain_id", "default"
'username': request.META['headers']['username'], ),
'user_id': request.META['headers']['user_id'], "username": request.META["headers"]["username"],
'authenticated': request.META['headers']['authenticated'] "user_id": request.META["headers"]["user_id"],
"authenticated": request.META["headers"]["authenticated"],
} }
except KeyError: except KeyError:
token_data = {} token_data = {}
@ -86,29 +89,29 @@ class RequestLoggingMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
self.logger = getLogger('adjutant') self.logger = getLogger("adjutant")
def __call__(self, request): def __call__(self, request):
self.logger.info( self.logger.info(
'(%s) - <%s> %s [%s]', "(%s) - <%s> %s [%s]",
timezone.now(), timezone.now(),
request.method, request.method,
request.META['REMOTE_ADDR'], request.META["REMOTE_ADDR"],
request.get_full_path() request.get_full_path(),
) )
request.timer = time() request.timer = time()
response = self.get_response(request) response = self.get_response(request)
if hasattr(request, 'timer'): if hasattr(request, "timer"):
time_delta = time() - request.timer time_delta = time() - request.timer
else: else:
time_delta = -1 time_delta = -1
self.logger.info( self.logger.info(
'(%s) - <%s> [%s] - (%.1fs)', "(%s) - <%s> [%s] - (%.1fs)",
timezone.now(), timezone.now(),
response.status_code, response.status_code,
request.get_full_path(), request.get_full_path(),
time_delta time_delta,
) )
return response return response

View File

@ -17,11 +17,7 @@ from adjutant.api.models import Notification
def create_notification(task, notes, error=False, handlers=True): def create_notification(task, notes, error=False, handlers=True):
notification = Notification.objects.create( notification = Notification.objects.create(task=task, notes=notes, error=error)
task=task,
notes=notes,
error=error
)
notification.save() notification.save()
if not handlers: if not handlers:

View File

@ -34,7 +34,8 @@ class BaseNotificationHandler(object):
""" """
try: try:
notif_config = CONF.notifications.handler_defaults.get( notif_config = CONF.notifications.handler_defaults.get(
self.__class__.__name__) self.__class__.__name__
)
except KeyError: except KeyError:
# Handler has no config # Handler has no config
return {} return {}
@ -44,10 +45,12 @@ class BaseNotificationHandler(object):
try: try:
if notification.error: if notification.error:
task_defaults = task_defaults.error_handler_config.get( task_defaults = task_defaults.error_handler_config.get(
self.__class__.__name__) self.__class__.__name__
)
else: else:
task_defaults = task_defaults.standard_handler_config.get( task_defaults = task_defaults.standard_handler_config.get(
self.__class__.__name__) self.__class__.__name__
)
except KeyError: except KeyError:
task_defaults = {} task_defaults = {}

View File

@ -57,12 +57,11 @@ class EmailNotification(base.BaseNotificationHandler):
fields.StrConfig( fields.StrConfig(
"template", "template",
help_text="Email template for this notification. " 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", default="notification.txt",
), ),
fields.StrConfig( fields.StrConfig(
"html_template", "html_template", help_text="Email html template for this notification.",
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.api.models import Notification
from adjutant.tasks.models import Task from adjutant.tasks.models import Task
from adjutant.common.tests.fake_clients import ( from adjutant.common.tests.fake_clients import FakeManager, setup_identity_cache
FakeManager, setup_identity_cache)
from adjutant.common.tests.utils import AdjutantAPITestCase from adjutant.common.tests.utils import AdjutantAPITestCase
from adjutant.config import CONF from adjutant.config import CONF
from adjutant import exceptions from adjutant import exceptions
@mock.patch('adjutant.common.user_store.IdentityManager', @mock.patch("adjutant.common.user_store.IdentityManager", FakeManager)
FakeManager)
@conf_utils.modify_conf( @conf_utils.modify_conf(
CONF, CONF,
operations={ operations={
"adjutant.workflow.tasks.create_project_and_user.notifications": [ "adjutant.workflow.tasks.create_project_and_user.notifications": [
{'operation': 'override', 'value': { {
"standard_handlers": ["EmailNotification"], "operation": "override",
"error_handlers": ["EmailNotification"], "value": {
"standard_handler_config": { "standard_handlers": ["EmailNotification"],
"EmailNotification": { "error_handlers": ["EmailNotification"],
'emails': ['example_notification@example.com'], "standard_handler_config": {
'reply': 'no-reply@example.com', "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): class NotificationTests(AdjutantAPITestCase):
def test_new_project_sends_notification(self): def test_new_project_sends_notification(self):
""" """
Confirm that the email notification handler correctly acknowledges Confirm that the email notification handler correctly acknowledges
@ -65,15 +66,15 @@ class NotificationTests(AdjutantAPITestCase):
setup_identity_cache() setup_identity_cache()
url = "/v1/openstack/sign-up" url = "/v1/openstack/sign-up"
data = {'project_name': "test_project", 'email': "test@example.com"} data = {"project_name": "test_project", "email": "test@example.com"}
response = self.client.post(url, data, format='json') response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
new_task = Task.objects.all()[0] new_task = Task.objects.all()[0]
self.assertEqual(Notification.objects.count(), 1) self.assertEqual(Notification.objects.count(), 1)
self.assertEqual(len(mail.outbox), 2) self.assertEqual(len(mail.outbox), 2)
self.assertEqual(mail.outbox[1].subject, "create_project_and_user notification") 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] notif = Notification.objects.all()[0]
self.assertEqual(notif.task.uuid, new_task.uuid) self.assertEqual(notif.task.uuid, new_task.uuid)
@ -81,12 +82,12 @@ class NotificationTests(AdjutantAPITestCase):
self.assertTrue(notif.acknowledged) self.assertTrue(notif.acknowledged)
headers = { headers = {
'project_name': "test_project", "project_name": "test_project",
'project_id': "test_project_id", "project_id": "test_project_id",
'roles': "admin,member", "roles": "admin,member",
'username': "test@example.com", "username": "test@example.com",
'user_id': "test_user_id", "user_id": "test_user_id",
'authenticated': True "authenticated": True,
} }
url = "/v1/tasks/" + new_task.uuid url = "/v1/tasks/" + new_task.uuid
with mock.patch( with mock.patch(
@ -102,8 +103,10 @@ class NotificationTests(AdjutantAPITestCase):
# should send token email, but no new notification # should send token email, but no new notification
self.assertEqual(Notification.objects.count(), 2) self.assertEqual(Notification.objects.count(), 2)
self.assertEqual(len(mail.outbox), 3) self.assertEqual(len(mail.outbox), 3)
self.assertEqual(mail.outbox[2].subject, "Error - create_project_and_user notification") self.assertEqual(
self.assertEqual(mail.outbox[2].to, ['example_error_notification@example.com']) 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] notif = Notification.objects.all()[1]
self.assertEqual(notif.task.uuid, new_task.uuid) 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 # Application definition
INSTALLED_APPS = ( INSTALLED_APPS = (
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.staticfiles', "django.contrib.staticfiles",
'rest_framework', "rest_framework",
'rest_framework_swagger', "rest_framework_swagger",
"adjutant.commands",
'adjutant.commands', "adjutant.actions",
'adjutant.actions', "adjutant.api",
'adjutant.api', "adjutant.notifications",
'adjutant.notifications', "adjutant.tasks",
'adjutant.tasks', "adjutant.startup",
'adjutant.startup',
) )
MIDDLEWARE = ( MIDDLEWARE = (
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'adjutant.middleware.KeystoneHeaderUnwrapper', "adjutant.middleware.KeystoneHeaderUnwrapper",
'adjutant.middleware.RequestLoggingMiddleware' "adjutant.middleware.RequestLoggingMiddleware",
) )
if 'test' in sys.argv: if "test" in sys.argv:
# modify MIDDLEWARE # modify MIDDLEWARE
MIDDLEWARE = list(MIDDLEWARE) MIDDLEWARE = list(MIDDLEWARE)
MIDDLEWARE.remove('adjutant.middleware.KeystoneHeaderUnwrapper') MIDDLEWARE.remove("adjutant.middleware.KeystoneHeaderUnwrapper")
MIDDLEWARE.append('adjutant.middleware.TestingHeaderUnwrapper') 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 USE_I18N = True
@ -75,33 +74,29 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
STATIC_URL = '/static/' STATIC_URL = "/static/"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'APP_DIRS': True, "APP_DIRS": True,
'NAME': 'default', "NAME": "default",
}, },
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'APP_DIRS': True, "APP_DIRS": True,
'DIRS': ['/etc/adjutant/templates/'], "DIRS": ["/etc/adjutant/templates/"],
'NAME': 'include_etc_templates', "NAME": "include_etc_templates",
}, },
] ]
AUTHENTICATION_BACKENDS = [] AUTHENTICATION_BACKENDS = []
REST_FRAMEWORK = { REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'adjutant.api.exception_handler.exception_handler', "EXCEPTION_HANDLER": "adjutant.api.exception_handler.exception_handler",
'DEFAULT_RENDERER_CLASSES': [ "DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer",],
'rest_framework.renderers.JSONRenderer', "DEFAULT_PARSER_CLASSES": ["rest_framework.parsers.JSONParser",],
], "DEFAULT_PERMISSION_CLASSES": [],
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
],
'DEFAULT_PERMISSION_CLASSES': [],
} }
SECRET_KEY = adj_conf.django.secret_key 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = adj_conf.django.debug DEBUG = adj_conf.django.debug
if DEBUG: if DEBUG:
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append( REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append(
'rest_framework.renderers.BrowsableAPIRenderer') "rest_framework.renderers.BrowsableAPIRenderer"
)
ALLOWED_HOSTS = adj_conf.django.allowed_hosts ALLOWED_HOSTS = adj_conf.django.allowed_hosts
SECURE_PROXY_SSL_HEADER = ( SECURE_PROXY_SSL_HEADER = (
adj_conf.django.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 DATABASES = adj_conf.django.databases
@ -125,30 +121,22 @@ if adj_conf.django.logging:
LOGGING = adj_conf.django.logging LOGGING = adj_conf.django.logging
else: else:
LOGGING = { LOGGING = {
'version': 1, "version": 1,
'disable_existing_loggers': False, "disable_existing_loggers": False,
'handlers': { "handlers": {
'file': { "file": {
'level': 'INFO', "level": "INFO",
'class': 'logging.FileHandler', "class": "logging.FileHandler",
'filename': adj_conf.django.log_file, "filename": adj_conf.django.log_file,
}, },
}, },
'loggers': { "loggers": {
'adjutant': { "adjutant": {"handlers": ["file"], "level": "INFO", "propagate": False,},
'handlers': ['file'], "django": {"handlers": ["file"], "level": "INFO", "propagate": False,},
'level': 'INFO', "keystonemiddleware": {
'propagate': False, "handlers": ["file"],
}, "level": "INFO",
'django': { "propagate": False,
'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(): def check_expected_delegate_apis():
missing_delegate_apis = list( missing_delegate_apis = list(
set(CONF.api.active_delegate_apis) set(CONF.api.active_delegate_apis) - set(api.DELEGATE_API_CLASSES.keys())
- set(api.DELEGATE_API_CLASSES.keys())) )
if missing_delegate_apis: if missing_delegate_apis:
raise DelegateAPINotRegistered( raise DelegateAPINotRegistered(
message=( message=(
"Expected DelegateAPIs are unregistered: %s" "Expected DelegateAPIs are unregistered: %s" % missing_delegate_apis
% missing_delegate_apis)) )
)
def check_configured_actions(): def check_configured_actions():
@ -38,11 +39,12 @@ def check_configured_actions():
configured_actions += task_class.default_actions configured_actions += task_class.default_actions
configured_actions += CONF.workflow.tasks.get( configured_actions += CONF.workflow.tasks.get(
task_class.task_type).additional_actions task_class.task_type
).additional_actions
missing_actions = list( missing_actions = list(set(configured_actions) - set(actions.ACTION_CLASSES.keys()))
set(configured_actions) - set(actions.ACTION_CLASSES.keys()))
if missing_actions: if missing_actions:
raise ActionNotRegistered( 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(): 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 = entry_point.load()
feature_set().load() feature_set().load()

View File

@ -11,33 +11,51 @@ import jsonfield.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('api', '0005_auto_20190610_0209'), ("api", "0005_auto_20190610_0209"),
] ]
operations = [ operations = [
migrations.SeparateDatabaseAndState( migrations.SeparateDatabaseAndState(
state_operations=[ state_operations=[
migrations.CreateModel( migrations.CreateModel(
name='Task', name="Task",
fields=[ 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)), "uuid",
('ip_address', models.GenericIPAddressField()), models.CharField(
('keystone_user', jsonfield.fields.JSONField(default={})), default=adjutant.tasks.models.hex_uuid,
('project_id', models.CharField(db_index=True, max_length=64, null=True)), max_length=32,
('approved_by', jsonfield.fields.JSONField(default={})), primary_key=True,
('task_type', models.CharField(db_index=True, max_length=100)), serialize=False,
('action_notes', jsonfield.fields.JSONField(default={})), ),
('cancelled', models.BooleanField(db_index=True, default=False)), ),
('approved', models.BooleanField(db_index=True, default=False)), ("hash_key", models.CharField(db_index=True, max_length=64)),
('completed', models.BooleanField(db_index=True, default=False)), ("ip_address", models.GenericIPAddressField()),
('created_on', models.DateTimeField(default=django.utils.timezone.now)), ("keystone_user", jsonfield.fields.JSONField(default={})),
('approved_on', models.DateTimeField(null=True)), (
('completed_on', models.DateTimeField(null=True)), "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={ options={"indexes": [],},
'indexes': [],
},
), ),
], ],
), ),

View File

@ -9,71 +9,77 @@ import jsonfield.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('tasks', '0001_initial'), ("tasks", "0001_initial"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(model_name="task", name="ip_address",),
model_name='task',
name='ip_address',
),
migrations.AddField( migrations.AddField(
model_name='task', model_name="task",
name='task_notes', name="task_notes",
field=jsonfield.fields.JSONField(default=[]), field=jsonfield.fields.JSONField(default=[]),
), ),
migrations.AlterField( migrations.AlterField(
model_name='task', model_name="task",
name='approved', name="approved",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
migrations.AlterField( migrations.AlterField(
model_name='task', model_name="task",
name='cancelled', name="cancelled",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
migrations.AlterField( migrations.AlterField(
model_name='task', model_name="task",
name='completed', name="completed",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
migrations.AlterField( migrations.AlterField(
model_name='task', model_name="task", name="hash_key", field=models.CharField(max_length=64),
name='hash_key',
field=models.CharField(max_length=64),
), ),
migrations.AlterField( migrations.AlterField(
model_name='task', model_name="task",
name='project_id', name="project_id",
field=models.CharField(max_length=64, null=True), field=models.CharField(max_length=64, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='task', model_name="task", name="task_type", field=models.CharField(max_length=100),
name='task_type',
field=models.CharField(max_length=100),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='task', model_name="task",
index=models.Index(fields=['completed'], name='completed_idx'), index=models.Index(fields=["completed"], name="completed_idx"),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='task', model_name="task",
index=models.Index(fields=['project_id', 'uuid'], name='tasks_task_project_a1cfa7_idx'), index=models.Index(
fields=["project_id", "uuid"], name="tasks_task_project_a1cfa7_idx"
),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='task', model_name="task",
index=models.Index(fields=['project_id', 'task_type'], name='tasks_task_project_e86456_idx'), index=models.Index(
fields=["project_id", "task_type"], name="tasks_task_project_e86456_idx"
),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='task', model_name="task",
index=models.Index(fields=['project_id', 'task_type', 'cancelled'], name='tasks_task_project_f0ec0e_idx'), index=models.Index(
fields=["project_id", "task_type", "cancelled"],
name="tasks_task_project_f0ec0e_idx",
),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='task', model_name="task",
index=models.Index(fields=['project_id', 'task_type', 'completed', 'cancelled'], name='tasks_task_project_1cb2a8_idx'), index=models.Index(
fields=["project_id", "task_type", "completed", "cancelled"],
name="tasks_task_project_1cb2a8_idx",
),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='task', model_name="task",
index=models.Index(fields=['hash_key', 'completed', 'cancelled'], name='tasks_task_hash_ke_781b6a_idx'), 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 Stores the state of the Task and a log for the
action. 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) hash_key = models.CharField(max_length=64)
# who is this: # who is this:
@ -61,13 +61,12 @@ class Task(models.Model):
class Meta: class Meta:
indexes = [ indexes = [
models.Index(fields=['completed'], name='completed_idx'), models.Index(fields=["completed"], name="completed_idx"),
models.Index(fields=['project_id', 'uuid']), models.Index(fields=["project_id", "uuid"]),
models.Index(fields=['project_id', 'task_type']), models.Index(fields=["project_id", "task_type"]),
models.Index(fields=['project_id', 'task_type', 'cancelled']), models.Index(fields=["project_id", "task_type", "cancelled"]),
models.Index(fields=[ models.Index(fields=["project_id", "task_type", "completed", "cancelled"]),
'project_id', 'task_type', 'completed', 'cancelled']), models.Index(fields=["hash_key", "completed", "cancelled"]),
models.Index(fields=['hash_key', 'completed', 'cancelled']),
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -89,7 +88,7 @@ class Task(models.Model):
@property @property
def actions(self): def actions(self):
return self.action_set.order_by('order') return self.action_set.order_by("order")
@property @property
def tokens(self): def tokens(self):
@ -102,11 +101,13 @@ class Task(models.Model):
def to_dict(self): def to_dict(self):
actions = [] actions = []
for action in self.actions: for action in self.actions:
actions.append({ actions.append(
"action_name": action.action_name, {
"data": action.action_data, "action_name": action.action_name,
"valid": action.valid "data": action.action_data,
}) "valid": action.valid,
}
)
return { return {
"uuid": self.uuid, "uuid": self.uuid,

View File

@ -23,8 +23,7 @@ from adjutant.api.models import Task
from adjutant.config import CONF from adjutant.config import CONF
from django.utils import timezone from django.utils import timezone
from adjutant.notifications.utils import create_notification from adjutant.notifications.utils import create_notification
from adjutant.tasks.v1.utils import ( from adjutant.tasks.v1.utils import send_stage_email, create_token, handle_task_error
send_stage_email, create_token, handle_task_error)
from adjutant import exceptions from adjutant import exceptions
@ -35,7 +34,7 @@ def make_task_config(task_class):
fields.BoolConfig( fields.BoolConfig(
"allow_auto_approve", "allow_auto_approve",
help_text="Override if this task allows auto_approval. " help_text="Override if this task allows auto_approval. "
"Otherwise uses task default.", "Otherwise uses task default.",
default=task_class.allow_auto_approve, default=task_class.allow_auto_approve,
) )
) )
@ -43,7 +42,7 @@ def make_task_config(task_class):
fields.ListConfig( fields.ListConfig(
"additional_actions", "additional_actions",
help_text="Additional actions to be run as part of the task " help_text="Additional actions to be run as part of the task "
"after default actions.", "after default actions.",
default=task_class.additional_actions or [], default=task_class.additional_actions or [],
) )
) )
@ -51,7 +50,7 @@ def make_task_config(task_class):
fields.IntConfig( fields.IntConfig(
"token_expiry", "token_expiry",
help_text="Override for the task token expiry. " help_text="Override for the task token expiry. "
"Otherwise uses task default.", "Otherwise uses task default.",
default=task_class.token_expiry, default=task_class.token_expiry,
) )
) )
@ -59,13 +58,11 @@ def make_task_config(task_class):
fields.DictConfig( fields.DictConfig(
"actions", "actions",
help_text="Action config overrides over the action defaults. " help_text="Action config overrides over the action defaults. "
"See 'adjutant.workflow.action_defaults'.", "See 'adjutant.workflow.action_defaults'.",
is_json=True, is_json=True,
default=task_class.action_config or {}, default=task_class.action_config or {},
sample_default={ sample_default={
"SomeCustomAction": { "SomeCustomAction": {"some_action_setting": "<a-uuid-probably>"}
"some_action_setting": "<a-uuid-probably>"
}
}, },
) )
) )
@ -73,14 +70,12 @@ def make_task_config(task_class):
fields.DictConfig( fields.DictConfig(
"emails", "emails",
help_text="Email config overrides for this task over task defaults." help_text="Email config overrides for this task over task defaults."
"See 'adjutant.workflow.emails'.", "See 'adjutant.workflow.emails'.",
is_json=True, is_json=True,
default=task_class.email_config or {}, default=task_class.email_config or {},
sample_default={ sample_default={
"initial": None, "initial": None,
"token": { "token": {"subject": "Some custom subject",},
"subject": "Some custom subject",
},
}, },
) )
) )
@ -88,7 +83,7 @@ def make_task_config(task_class):
fields.DictConfig( fields.DictConfig(
"notifications", "notifications",
help_text="Notification config overrides for this task over task defaults." help_text="Notification config overrides for this task over task defaults."
"See 'adjutant.workflow.notifications'.", "See 'adjutant.workflow.notifications'.",
is_json=True, is_json=True,
default=task_class.notification_config or {}, default=task_class.notification_config or {},
sample_default={ sample_default={
@ -96,14 +91,14 @@ def make_task_config(task_class):
"error_handlers": ["EmailNotification"], "error_handlers": ["EmailNotification"],
"standard_handler_config": { "standard_handler_config": {
"EmailNotification": { "EmailNotification": {
'emails': ['example@example.com'], "emails": ["example@example.com"],
'reply': 'no-reply@example.com', "reply": "no-reply@example.com",
} }
}, },
"error_handler_config": { "error_handler_config": {
"EmailNotification": { "EmailNotification": {
'emails': ['example@example.com'], "emails": ["example@example.com"],
'reply': 'no-reply@example.com', "reply": "no-reply@example.com",
} }
}, },
}, },
@ -141,50 +136,45 @@ class BaseTask(object):
email_config = None email_config = None
notification_config = None notification_config = None
def __init__(self, def __init__(self, task_model=None, task_data=None, action_data=None):
task_model=None,
task_data=None,
action_data=None):
self._config = None self._config = None
self.logger = getLogger('adjutant') self.logger = getLogger("adjutant")
if task_model: if task_model:
self.task = task_model self.task = task_model
self._refresh_actions() self._refresh_actions()
else: else:
# raises 400 validation error # raises 400 validation error
action_serializer_list = self._instantiate_action_serializers( action_serializer_list = self._instantiate_action_serializers(action_data)
action_data)
hash_key = self._create_task_hash(action_serializer_list) hash_key = self._create_task_hash(action_serializer_list)
# raises duplicate error # raises duplicate error
self._handle_duplicates(hash_key) self._handle_duplicates(hash_key)
keystone_user = task_data.get('keystone_user', {}) keystone_user = task_data.get("keystone_user", {})
self.task = Task.objects.create( self.task = Task.objects.create(
keystone_user=keystone_user, keystone_user=keystone_user,
project_id=keystone_user.get('project_id'), project_id=keystone_user.get("project_id"),
task_type=self.task_type, task_type=self.task_type,
hash_key=hash_key) hash_key=hash_key,
)
self.task.save() self.task.save()
# Instantiate actions with serializers # Instantiate actions with serializers
self.actions = [] self.actions = []
for i, action in enumerate(action_serializer_list): for i, action in enumerate(action_serializer_list):
data = action['serializer'].validated_data data = action["serializer"].validated_data
# construct the action class # construct the action class
self.actions.append(action['action']( self.actions.append(
data=data, action["action"](data=data, task=self.task, order=i)
task=self.task, )
order=i
))
self.logger.info( self.logger.info(
"(%s) - '%s' task created (%s)." "(%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, def _instantiate_action_serializers(self, action_data, use_existing_actions=False):
use_existing_actions=False):
action_serializer_list = [] action_serializer_list = []
if use_existing_actions: if use_existing_actions:
@ -209,13 +199,13 @@ class BaseTask(object):
# instantiate serializer class # instantiate serializer class
if not action_class.serializer: if not action_class.serializer:
raise exceptions.SerializerMissingException( 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) serializer = action_class.serializer(data=action_data)
action_serializer_list.append({ action_serializer_list.append(
'name': action_name, {"name": action_name, "action": action_class, "serializer": serializer}
'action': action_class, )
'serializer': serializer})
if serializer and not serializer.is_valid(): if serializer and not serializer.is_valid():
valid = False valid = False
@ -223,52 +213,50 @@ class BaseTask(object):
if not valid: if not valid:
errors = {} errors = {}
for action in action_serializer_list: for action in action_serializer_list:
if action['serializer']: if action["serializer"]:
errors.update(action['serializer'].errors) errors.update(action["serializer"].errors)
raise exceptions.TaskSerializersInvalid(errors) raise exceptions.TaskSerializersInvalid(errors)
return action_serializer_list return action_serializer_list
def _create_task_hash(self, action_list): def _create_task_hash(self, action_list):
hashable_list = [self.task_type, ] hashable_list = [
self.task_type,
]
for action in action_list: for action in action_list:
hashable_list.append(action['name']) hashable_list.append(action["name"])
if not action['serializer']: if not action["serializer"]:
continue continue
# iterate like this to maintain consistent order for hash # 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: for field in fields:
try: try:
hashable_list.append( hashable_list.append(action["serializer"].validated_data[field])
action['serializer'].validated_data[field])
except KeyError: except KeyError:
if field == "username" and CONF.identity.username_is_email: if field == "username" and CONF.identity.username_is_email:
continue continue
else: else:
raise 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): def _handle_duplicates(self, hash_key):
duplicate_tasks = Task.objects.filter( duplicate_tasks = Task.objects.filter(
hash_key=hash_key, hash_key=hash_key, completed=0, cancelled=0
completed=0, )
cancelled=0)
if not duplicate_tasks: if not duplicate_tasks:
return return
if self.duplicate_policy == "cancel": if self.duplicate_policy == "cancel":
now = timezone.now() now = timezone.now()
self.logger.info( self.logger.info("(%s) - Task is a duplicate - Cancelling old tasks." % now)
"(%s) - Task is a duplicate - Cancelling old tasks." %
now)
for task in duplicate_tasks: for task in duplicate_tasks:
task.add_task_note( task.add_task_note(
"Task cancelled because was an old duplicate. - (%s)" "Task cancelled because was an old duplicate. - (%s)" % now
% now) )
task.get_task().cancel() task.get_task().cancel()
return return
@ -288,7 +276,7 @@ class BaseTask(object):
email_conf = self.config.emails.token email_conf = self.config.emails.token
send_stage_email(self.task, email_conf, token) send_stage_email(self.task, email_conf, token)
except KeyError as e: 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): def add_note(self, note):
""" """
@ -296,7 +284,8 @@ class BaseTask(object):
""" """
now = timezone.now() now = timezone.now()
self.logger.info( 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) note = "%s - (%s)" % (note, now)
self.task.add_task_note(note) self.task.add_task_note(note)
@ -320,7 +309,8 @@ class BaseTask(object):
if not valid: if not valid:
# TODO(amelia): get action invalidation reasons and raise those # TODO(amelia): get action invalidation reasons and raise those
raise exceptions.TaskActionsInvalid( raise exceptions.TaskActionsInvalid(
self.task, 'actions invalid', internal_message) self.task, "actions invalid", internal_message
)
@property @property
def approved(self): def approved(self):
@ -342,40 +332,47 @@ class BaseTask(object):
if completed is not None: if completed is not None:
if self.task.completed and not completed: if self.task.completed and not completed:
raise exceptions.TaskStateInvalid( 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: if not self.task.completed and completed:
raise exceptions.TaskStateInvalid( 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 cancelled is not None:
if self.task.cancelled and not cancelled: if self.task.cancelled and not cancelled:
raise exceptions.TaskStateInvalid( raise exceptions.TaskStateInvalid(
self.task, "This task has been cancelled.") self.task, "This task has been cancelled."
)
if not self.task.cancelled and cancelled: if not self.task.cancelled and cancelled:
raise exceptions.TaskStateInvalid( 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 approved is not None:
if self.task.approved and not approved: if self.task.approved and not approved:
raise exceptions.TaskStateInvalid( 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: if not self.task.approved and approved:
raise exceptions.TaskStateInvalid( raise exceptions.TaskStateInvalid(
self.task, "This task has not been approved.") self.task, "This task has not been approved."
)
def update(self, action_data): def update(self, action_data):
self.confirm_state(approved=False, completed=False, cancelled=False) self.confirm_state(approved=False, completed=False, cancelled=False)
action_serializer_list = self._instantiate_action_serializers( 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) hash_key = self._create_task_hash(action_serializer_list)
self._handle_duplicates(hash_key) self._handle_duplicates(hash_key)
for action in action_serializer_list: 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.action_data = data
action['action'].action.save() action["action"].action.save()
self._refresh_actions() self._refresh_actions()
self.prepare() self.prepare()
@ -392,8 +389,7 @@ class BaseTask(object):
try: try:
action.prepare() action.prepare()
except Exception as e: except Exception as e:
handle_task_error( handle_task_error(e, self.task, error_text="while setting up task")
e, self.task, error_text='while setting up task')
# send initial confirmation email: # send initial confirmation email:
email_conf = self.config.emails.initial email_conf = self.config.emails.initial
@ -424,10 +420,7 @@ class BaseTask(object):
return return
if self.send_approval_notification: if self.send_approval_notification:
notes = { notes = {"notes": ["'%s' task needs approval." % self.task_type]}
'notes':
["'%s' task needs approval." % self.task_type]
}
create_notification(self.task, notes) create_notification(self.task, notes)
def approve(self, approved_by="system"): def approve(self, approved_by="system"):
@ -451,8 +444,7 @@ class BaseTask(object):
try: try:
action.approve() action.approve()
except Exception as e: except Exception as e:
handle_task_error( handle_task_error(e, self.task, error_text="while approving task")
e, self.task, error_text='while approving task')
self.is_valid("task invalid after approval") self.is_valid("task invalid after approval")
@ -495,10 +487,11 @@ class BaseTask(object):
try: try:
data[field] = token_data[field] data[field] = token_data[field]
except KeyError: except KeyError:
errors[field] = ["This field is required.", ] errors[field] = [
"This field is required.",
]
except TypeError: except TypeError:
errors = ["Improperly formated json. " errors = ["Improperly formated json. " "Should be a key-value object."]
"Should be a key-value object."]
break break
if errors: if errors:
@ -510,8 +503,7 @@ class BaseTask(object):
try: try:
action.submit(data) action.submit(data)
except Exception as e: except Exception as e:
handle_task_error( handle_task_error(e, self.task, "while submiting task")
e, self.task, "while submiting task")
self.is_valid("task invalid after submit") self.is_valid("task invalid after submit")

View File

@ -23,9 +23,8 @@ from adjutant.tasks.v1.base import BaseTask
class TaskManager(object): class TaskManager(object):
def __init__(self, message=None): def __init__(self, message=None):
self.logger = getLogger('adjutant') self.logger = getLogger("adjutant")
def _get_task_class(self, task_type): def _get_task_class(self, task_type):
"""Get the task class from the given task_type """Get the task class from the given task_type
@ -38,8 +37,7 @@ class TaskManager(object):
except KeyError: except KeyError:
if task_type in tasks.TASK_CLASSES.values(): if task_type in tasks.TASK_CLASSES.values():
return task_type return task_type
raise exceptions.TaskNotRegistered( raise exceptions.TaskNotRegistered("Unknown task type: '%s'" % task_type)
"Unknown task type: '%s'" % task_type)
def create_from_request(self, task_type, request): def create_from_request(self, task_type, request):
task_class = self._get_task_class(task_type) task_class = self._get_task_class(task_type)
@ -65,7 +63,8 @@ class TaskManager(object):
task = Task.objects.get(uuid=task) task = Task.objects.get(uuid=task)
except Task.DoesNotExist: except Task.DoesNotExist:
raise exceptions.TaskNotFound( raise exceptions.TaskNotFound(
"Task not found with uuid of: '%s'" % task) "Task not found with uuid of: '%s'" % task
)
if isinstance(task, Task): if isinstance(task, Task):
try: try:
return tasks.TASK_CLASSES[task.task_type](task) return tasks.TASK_CLASSES[task.task_type](task)
@ -74,11 +73,9 @@ class TaskManager(object):
# for older deprecated tasks: # for older deprecated tasks:
raise exceptions.TaskNotRegistered( raise exceptions.TaskNotRegistered(
"Task type '%s' not registered, " "Task type '%s' not registered, "
"and used for existing task." "and used for existing task." % task.task_type
% task.task_type
) )
raise exceptions.TaskNotFound( raise exceptions.TaskNotFound("Task not found for value of: '%s'" % task)
"Task not found for value of: '%s'" % task)
def update(self, task, action_data): def update(self, task, action_data):
task = self.get(task) task = self.get(task)

View File

@ -18,22 +18,22 @@ from adjutant.tasks.v1.base import BaseTask
class CreateProjectAndUser(BaseTask): class CreateProjectAndUser(BaseTask):
duplicate_policy = "block" duplicate_policy = "block"
task_type = "create_project_and_user" task_type = "create_project_and_user"
deprecated_task_types = ['create_project', 'signup'] deprecated_task_types = ["create_project", "signup"]
default_actions = [ default_actions = [
"NewProjectWithUserAction", "NewProjectWithUserAction",
] ]
email_config = { email_config = {
'initial': { "initial": {
'template': 'create_project_and_user_initial.txt', "template": "create_project_and_user_initial.txt",
'subject': 'signup received' "subject": "signup received",
}, },
'token': { "token": {
'template': 'create_project_and_user_token.txt', "template": "create_project_and_user_token.txt",
'subject': 'signup approved' "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 = { email_config = {
'initial': None, "initial": None,
'token': None, "token": None,
'completed': { "completed": {
'template': 'create_project_and_user_completed.txt', "template": "create_project_and_user_completed.txt",
'subject': 'signup completed' "subject": "signup completed",
} },
} }

View File

@ -18,84 +18,80 @@ from adjutant.tasks.v1.base import BaseTask
class InviteUser(BaseTask): class InviteUser(BaseTask):
duplicate_policy = "block" duplicate_policy = "block"
task_type = "invite_user_to_project" task_type = "invite_user_to_project"
deprecated_task_types = ['invite_user'] deprecated_task_types = ["invite_user"]
default_actions = [ default_actions = [
"NewUserAction", "NewUserAction",
] ]
email_config = { email_config = {
'initial': None, "initial": None,
'token': { "token": {
'template': 'invite_user_to_project_token.txt', "template": "invite_user_to_project_token.txt",
'subject': 'invite_user_to_project' "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): class ResetUserPassword(BaseTask):
task_type = "reset_user_password" task_type = "reset_user_password"
deprecated_task_types = ['reset_password'] deprecated_task_types = ["reset_password"]
default_actions = [ default_actions = [
"ResetUserPasswordAction", "ResetUserPasswordAction",
] ]
email_config = { email_config = {
'initial': None, "initial": None,
'token': { "token": {
'template': 'reset_user_password_token.txt', "template": "reset_user_password_token.txt",
'subject': 'Password Reset for OpenStack' "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): class EditUserRoles(BaseTask):
task_type = "edit_user_roles" task_type = "edit_user_roles"
deprecated_task_types = ['edit_user'] deprecated_task_types = ["edit_user"]
default_actions = [ default_actions = [
"EditUserRolesAction", "EditUserRolesAction",
] ]
email_config = { email_config = {"initial": None, "token": None, "completed": None}
'initial': None,
'token': None,
'completed': None
}
class UpdateUserEmail(BaseTask): class UpdateUserEmail(BaseTask):
task_type = "update_user_email" task_type = "update_user_email"
deprecated_task_types = ['update_email'] deprecated_task_types = ["update_email"]
default_actions = [ default_actions = [
"UpdateUserEmailAction", "UpdateUserEmailAction",
] ]
additional_actions = [ additional_actions = [
'SendAdditionalEmailAction', "SendAdditionalEmailAction",
] ]
action_config = { action_config = {
'SendAdditionalEmailAction': { "SendAdditionalEmailAction": {
'initial': { "initial": {
'subject': 'OpenStack Email Update Requested', "subject": "OpenStack Email Update Requested",
'template': 'update_user_email_started.txt', "template": "update_user_email_started.txt",
'email_current_user': True, "email_current_user": True,
}, },
}, },
} }
email_config = { email_config = {
'initial': None, "initial": None,
'token': { "token": {
'subject': 'update_user_email_token', "subject": "update_user_email_token",
'template': 'update_user_email_token.txt' "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 from adjutant import exceptions
LOG = getLogger('adjutant') LOG = getLogger("adjutant")
def handle_task_error(e, task, error_text="while running task"): def handle_task_error(e, task, error_text="while running task"):
import traceback 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." trace = traceback.format_exc()
% (type(e).__name__, e, error_text)] 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) 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) expire = timezone.now() + timedelta(seconds=expiry_time)
uuid = uuid4().hex uuid = uuid4().hex
token = Token.objects.create( token = Token.objects.create(task=task, token=uuid, expires=expire)
task=task,
token=uuid,
expires=expire
)
token.save() token.save()
return token return token
@ -63,13 +62,13 @@ def send_stage_email(task, email_conf, token=None):
return return
text_template = loader.get_template( text_template = loader.get_template(
email_conf['template'], email_conf["template"], using="include_etc_templates"
using='include_etc_templates') )
html_template = email_conf['html_template'] html_template = email_conf["html_template"]
if html_template: if html_template:
html_template = loader.get_template( html_template = loader.get_template(
html_template, html_template, using="include_etc_templates"
using='include_etc_templates') )
emails = set() emails = set()
actions = {} actions = {}
@ -86,74 +85,62 @@ def send_stage_email(task, email_conf, token=None):
if len(emails) > 1: if len(emails) > 1:
notes = { notes = {
'errors': "errors": (
("Error: Unable to send update, more than one email for task: %s" "Error: Unable to send update, more than one email for task: %s"
% task.uuid) % task.uuid
)
} }
create_notification(task, notes, error=True) create_notification(task, notes, error=True)
return return
context = { context = {"task": task, "actions": actions}
'task': task,
'actions': actions
}
if token: if token:
tokenurl = CONF.workflow.horizon_url tokenurl = CONF.workflow.horizon_url
if not tokenurl.endswith('/'): if not tokenurl.endswith("/"):
tokenurl += '/' tokenurl += "/"
tokenurl += 'token/' tokenurl += "token/"
context.update({ context.update({"tokenurl": tokenurl, "token": token.token})
'tokenurl': tokenurl,
'token': token.token
})
try: try:
message = text_template.render(context) message = text_template.render(context)
# from_email is the return-path and is distinct from the # from_email is the return-path and is distinct from the
# message headers # message headers
from_email = email_conf['from'] from_email = email_conf["from"]
if not from_email: if not from_email:
from_email = email_conf['reply'] from_email = email_conf["reply"]
elif "%(task_uuid)s" in from_email: 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 # these are the message headers which will be visible to
# the email client. # the email client.
headers = { 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 needs to be set to be disctinct from return-path
'From': email_conf['reply'], "From": email_conf["reply"],
'Reply-To': email_conf['reply'], "Reply-To": email_conf["reply"],
} }
email = EmailMultiAlternatives( email = EmailMultiAlternatives(
email_conf['subject'], email_conf["subject"], message, from_email, [emails.pop()], headers=headers,
message,
from_email,
[emails.pop()],
headers=headers,
) )
if html_template: if html_template:
email.attach_alternative( email.attach_alternative(html_template.render(context), "text/html")
html_template.render(context), "text/html")
email.send(fail_silently=False) email.send(fail_silently=False)
except Exception as e: except Exception as e:
notes = { notes = {
'errors': "errors": (
("Error: '%s' while emailing update for task: %s" % "Error: '%s' while emailing update for task: %s" % (e, task.uuid)
(e, task.uuid)) )
} }
notif_conf = task.config.notifications notif_conf = task.config.notifications
if e.__class__.__name__ in notif_conf.safe_errors: if e.__class__.__name__ in notif_conf.safe_errors:
notification = create_notification( notification = create_notification(task, notes, error=True, handlers=False)
task, notes, error=True,
handlers=False)
notification.acknowledged = True notification.acknowledged = True
notification.save() notification.save()
else: else:

View File

@ -15,5 +15,5 @@
from django.conf.urls import include, url from django.conf.urls import include, url
urlpatterns = [ 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. # the Keystone Auth Middleware.
conf = { conf = {
"auth_plugin": "password", "auth_plugin": "password",
'username': CONF.identity.auth.username, "username": CONF.identity.auth.username,
'password': CONF.identity.auth.password, "password": CONF.identity.auth.password,
'project_name': CONF.identity.auth.project_name, "project_name": CONF.identity.auth.project_name,
"project_domain_id": CONF.identity.auth.project_domain_id, "project_domain_id": CONF.identity.auth.project_domain_id,
"user_domain_id": CONF.identity.auth.user_domain_id, "user_domain_id": CONF.identity.auth.user_domain_id,
"auth_url": CONF.identity.auth.auth_url, "auth_url": CONF.identity.auth.auth_url,
'delay_auth_decision': True, "delay_auth_decision": True,
'include_service_catalog': False, "include_service_catalog": False,
'token_cache_time': CONF.identity.token_cache_time, "token_cache_time": CONF.identity.token_cache_time,
} }
application = AuthProtocol(application, conf) application = AuthProtocol(application, conf)

View File

@ -29,12 +29,9 @@
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = ["os_api_ref", "openstackdocstheme"]
'os_api_ref',
'openstackdocstheme'
]
html_theme = 'openstackdocstheme' html_theme = "openstackdocstheme"
# Theme options are theme-specific and customize the look and feel of a theme # 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 # further. For a list of options available for each theme, see the
@ -42,37 +39,37 @@ html_theme = 'openstackdocstheme'
# html_theme_options = {} # html_theme_options = {}
# openstackdocstheme settings # openstackdocstheme settings
repository_name = 'openstack/adjutant' repository_name = "openstack/adjutant"
html_theme = 'openstackdocs' html_theme = "openstackdocs"
use_storyboard = True use_storyboard = True
# Add any paths that contain custom themes here, relative to this directory. # Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() # html_theme_path = sphinx_bootstrap_theme.get_html_theme_path()
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = '.rst' source_suffix = ".rst"
# The encoding of source files. # The encoding of source files.
# source_encoding = 'utf-8-sig' # source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = u'Adjutant API Reference' project = "Adjutant API Reference"
copyright = u'2017, Catalyst IT Ltd' copyright = "2017, Catalyst IT Ltd"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '' version = ""
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '' release = ""
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
@ -86,7 +83,7 @@ release = ''
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # 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 # The reST default role (used for this markup: `text`) to use for all
# documents. # documents.
@ -104,7 +101,7 @@ exclude_patterns = ['_build']
# show_authors = False # show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # 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. # A list of ignored prefixes for module index sorting.
# modindex_common_prefix = [] # modindex_common_prefix = []
@ -195,7 +192,7 @@ pygments_style = 'sphinx'
# html_file_suffix = None # html_file_suffix = None
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'AdjutantAPIReferencedoc' htmlhelp_basename = "AdjutantAPIReferencedoc"
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
@ -215,9 +212,13 @@ htmlhelp_basename = 'AdjutantAPIReferencedoc'
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
('index', 'AdjutantAPIReference.tex', (
u'Adjutant API Reference Documentation', "index",
u'Catalyst IT Ltd', 'manual'), "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 # 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 # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ 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. # If true, show URL addresses after external links.
@ -260,12 +266,17 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'AdjutantAPIReference', u'Adjutant API Reference Documentation', (
u'Catalyst IT Ltd', 'AdjutantAPIReference', "index",
'A simple workflow framework to help automate admin and user tasks in ' "AdjutantAPIReference",
'and around OpenStack via a pluggable API exposing tasks made up of ' "Adjutant API Reference Documentation",
'easily chainable actions.', "Catalyst IT Ltd",
'Miscellaneous'), "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. # 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. # 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 # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = ["openstackdocstheme"]
'openstackdocstheme'
]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# openstackdocstheme settings # openstackdocstheme settings
repository_name = 'openstack/adjutant' repository_name = "openstack/adjutant"
html_theme = 'openstackdocs' html_theme = "openstackdocs"
use_storyboard = True use_storyboard = True
# The suffix(es) of source filenames. # The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string: # You can specify multiple suffix as a list of string:
# #
# source_suffix = ['.rst', '.md'] # source_suffix = ['.rst', '.md']
source_suffix = '.rst' source_suffix = ".rst"
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = u'Adjutant' project = "Adjutant"
copyright = u'2017, Catalyst IT Ltd' copyright = "2017, Catalyst IT Ltd"
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
@ -62,7 +60,7 @@ copyright = u'2017, Catalyst IT Ltd'
exclude_patterns = [] exclude_patterns = []
# The name of the Pygments (syntax highlighting) style to use. # 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. # If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False todo_include_todos = False
@ -93,7 +91,7 @@ todo_include_todos = False
# -- Options for HTMLHelp output ------------------------------------------ # -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'Adjutantdoc' htmlhelp_basename = "Adjutantdoc"
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
@ -102,8 +100,7 @@ htmlhelp_basename = 'Adjutantdoc'
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
(master_doc, 'Adjutant.tex', u'Adjutant Documentation', (master_doc, "Adjutant.tex", "Adjutant Documentation", "Catalyst IT Ltd", "manual"),
u'Catalyst IT Ltd', 'manual'),
] ]
@ -111,10 +108,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [(master_doc, "adjutant", "Adjutant Documentation", ["Catalyst IT Ltd"], 1)]
(master_doc, 'adjutant', u'Adjutant Documentation',
['Catalyst IT Ltd'], 1)
]
# -- Options for Texinfo output ------------------------------------------- # -- Options for Texinfo output -------------------------------------------
@ -123,7 +117,13 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'Adjutant', u'Adjutant Documentation', (
'Catalyst IT Ltd', 'Adjutant', 'One line description of project.', master_doc,
'Miscellaneous'), "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 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'openstackdocstheme', "openstackdocstheme",
'reno.sphinxext', "reno.sphinxext",
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = '.rst' source_suffix = ".rst"
# The encoding of source files. # The encoding of source files.
# source_encoding = 'utf-8-sig' # source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = u'Adjutant Release Notes' project = "Adjutant Release Notes"
copyright = u'2019, Adjutant Developers' copyright = "2019, Adjutant Developers"
# openstackdocstheme settings # openstackdocstheme settings
repository_name = 'openstack/adjutant' repository_name = "openstack/adjutant"
use_storyboard = True use_storyboard = True
# Release notes are version independent # Release notes are version independent
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '' release = ""
# The short X.Y version. # The short X.Y version.
version = '' version = ""
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
@ -96,7 +96,7 @@ exclude_patterns = []
# show_authors = False # show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # 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. # A list of ignored prefixes for module index sorting.
# modindex_common_prefix = [] # modindex_common_prefix = []
@ -109,7 +109,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # 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 # 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 # further. For a list of options available for each theme, see the
@ -188,7 +188,7 @@ html_theme = 'openstackdocs'
# html_file_suffix = None # html_file_suffix = None
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'AdjutantReleaseNotesdoc' htmlhelp_basename = "AdjutantReleaseNotesdoc"
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
@ -197,9 +197,13 @@ htmlhelp_basename = 'AdjutantReleaseNotesdoc'
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
('index', 'AdjutantReleaseNotes.tex', (
u'Adjutant Release Notes Documentation', "index",
u'Adjutant Developers', 'manual'), "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 # 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 # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ 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. # If true, show URL addresses after external links.
@ -242,10 +251,15 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'AdjutantReleaseNotes', u'Adjutant Release Notes Documentation', (
u'Adjutant Developers', 'AdjutantReleaseNotes', "index",
'An extensible API framework for admin logic in OpenStack.', "AdjutantReleaseNotes",
'Miscellaneous'), "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. # Documents to append as an appendix to all manuals.
@ -261,4 +275,4 @@ texinfo_documents = [
# texinfo_no_detailmenu = False # texinfo_no_detailmenu = False
# -- Options for Internationalization output ------------------------------ # -- Options for Internationalization output ------------------------------
locale_dirs = ['locale/'] locale_dirs = ["locale/"]

View File

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

View File

@ -52,7 +52,7 @@ commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenote
[flake8] [flake8]
max-line-length = 88 max-line-length = 88
select = C,E,F,W,B,B950 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 show-source = true
builtins = _ builtins = _
exclude=.venv,venv,.env,env,.git,.tox,dist,doc,*lib/python*,*egg,releasenotes,adjutant/api/migrations/*,adjutant/actions/migrations,adjutant/tasks/migrations exclude=.venv,venv,.env,env,.git,.tox,dist,doc,*lib/python*,*egg,releasenotes,adjutant/api/migrations/*,adjutant/actions/migrations,adjutant/tasks/migrations