mirror of
https://github.com/ansible/awx.git
synced 2026-07-12 00:44:36 -02:30
Merge branch 'release_3.0.0' of github.com:ansible/ansible-tower into JobDetailsAudit
This commit is contained in:
@@ -219,7 +219,7 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
else:
|
else:
|
||||||
q = Q(**{k:v})
|
q = Q(**{k:v})
|
||||||
queryset = queryset.filter(q)
|
queryset = queryset.filter(q)
|
||||||
queryset = queryset.filter(*args)
|
queryset = queryset.filter(*args).distinct()
|
||||||
return queryset
|
return queryset
|
||||||
except (FieldError, FieldDoesNotExist, ValueError), e:
|
except (FieldError, FieldDoesNotExist, ValueError), e:
|
||||||
raise ParseError(e.args[0])
|
raise ParseError(e.args[0])
|
||||||
|
|||||||
@@ -1766,7 +1766,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
|||||||
notification_templates_any = reverse('api:job_template_notification_templates_any_list', args=(obj.pk,)),
|
notification_templates_any = reverse('api:job_template_notification_templates_any_list', args=(obj.pk,)),
|
||||||
notification_templates_success = reverse('api:job_template_notification_templates_success_list', args=(obj.pk,)),
|
notification_templates_success = reverse('api:job_template_notification_templates_success_list', args=(obj.pk,)),
|
||||||
notification_templates_error = reverse('api:job_template_notification_templates_error_list', args=(obj.pk,)),
|
notification_templates_error = reverse('api:job_template_notification_templates_error_list', args=(obj.pk,)),
|
||||||
access_list = reverse('api:job_template_access_list', args=(obj.pk,)),
|
access_list = reverse('api:job_template_access_list', args=(obj.pk,)),
|
||||||
survey_spec = reverse('api:job_template_survey_spec', args=(obj.pk,)),
|
survey_spec = reverse('api:job_template_survey_spec', args=(obj.pk,)),
|
||||||
labels = reverse('api:job_template_label_list', args=(obj.pk,)),
|
labels = reverse('api:job_template_label_list', args=(obj.pk,)),
|
||||||
roles = reverse('api:job_template_roles_list', args=(obj.pk,)),
|
roles = reverse('api:job_template_roles_list', args=(obj.pk,)),
|
||||||
|
|||||||
@@ -2212,6 +2212,13 @@ class JobTemplateList(ListCreateAPIView):
|
|||||||
serializer_class = JobTemplateSerializer
|
serializer_class = JobTemplateSerializer
|
||||||
always_allow_superuser = False
|
always_allow_superuser = False
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
ret = super(JobTemplateList, self).post(request, *args, **kwargs)
|
||||||
|
if ret.status_code == 201:
|
||||||
|
job_template = JobTemplate.objects.get(id=ret.data['id'])
|
||||||
|
job_template.admin_role.members.add(request.user)
|
||||||
|
return ret
|
||||||
|
|
||||||
class JobTemplateDetail(RetrieveUpdateDestroyAPIView):
|
class JobTemplateDetail(RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
|
|||||||
@@ -773,7 +773,9 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
inventory_pk = get_pk_from_dict(data, 'inventory')
|
inventory_pk = get_pk_from_dict(data, 'inventory')
|
||||||
inventory = Inventory.objects.filter(id=inventory_pk)
|
inventory = Inventory.objects.filter(id=inventory_pk)
|
||||||
if not inventory.exists() and not data.get('ask_inventory_on_launch', False):
|
if not inventory.exists() and not data.get('ask_inventory_on_launch', False):
|
||||||
return False # Does this make sense? Maybe should check read access
|
return False
|
||||||
|
if inventory.exists() and self.user not in inventory[0].use_role:
|
||||||
|
return False
|
||||||
|
|
||||||
project_pk = get_pk_from_dict(data, 'project')
|
project_pk = get_pk_from_dict(data, 'project')
|
||||||
if 'job_type' in data and data['job_type'] == PERM_INVENTORY_SCAN:
|
if 'job_type' in data and data['job_type'] == PERM_INVENTORY_SCAN:
|
||||||
@@ -786,10 +788,8 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
# If the user has admin access to the project (as an org admin), should
|
# If the user has admin access to the project (as an org admin), should
|
||||||
# be able to proceed without additional checks.
|
# be able to proceed without additional checks.
|
||||||
project = get_object_or_400(Project, pk=project_pk)
|
project = get_object_or_400(Project, pk=project_pk)
|
||||||
if self.user in project.admin_role:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return self.user in project.admin_role and self.user in inventory.read_role
|
return self.user in project.use_role
|
||||||
|
|
||||||
def can_start(self, obj, validate_license=True):
|
def can_start(self, obj, validate_license=True):
|
||||||
# Check license.
|
# Check license.
|
||||||
@@ -817,13 +817,62 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
if self.user not in obj.admin_role:
|
if self.user not in obj.admin_role:
|
||||||
return False
|
return False
|
||||||
if data is not None:
|
if data is not None:
|
||||||
data_for_change = dict(data)
|
data = dict(data)
|
||||||
|
|
||||||
|
if self.changes_are_non_sensitive(obj, data):
|
||||||
|
if 'job_type' in data and obj.job_type != data['job_type'] and data['job_type'] == PERM_INVENTORY_SCAN:
|
||||||
|
self.check_license(feature='system_tracking')
|
||||||
|
|
||||||
|
if 'survey_enabled' in data and obj.survey_enabled != data['survey_enabled'] and data['survey_enabled']:
|
||||||
|
self.check_license(feature='surveys')
|
||||||
|
return True
|
||||||
|
|
||||||
for required_field in ('credential', 'cloud_credential', 'inventory', 'project'):
|
for required_field in ('credential', 'cloud_credential', 'inventory', 'project'):
|
||||||
required_obj = getattr(obj, required_field, None)
|
required_obj = getattr(obj, required_field, None)
|
||||||
if required_field not in data_for_change and required_obj is not None:
|
if required_field not in data_for_change and required_obj is not None:
|
||||||
data_for_change[required_field] = required_obj.pk
|
data_for_change[required_field] = required_obj.pk
|
||||||
return self.can_read(obj) and self.can_add(data_for_change)
|
return self.can_read(obj) and self.can_add(data_for_change)
|
||||||
|
|
||||||
|
def changes_are_non_sensitive(self, obj, data):
|
||||||
|
'''
|
||||||
|
Returne true if the changes being made are considered nonsensitive, and
|
||||||
|
thus can be made by a job template administrator which may not have access
|
||||||
|
to the any inventory, project, or credentials associated with the template.
|
||||||
|
'''
|
||||||
|
# We are white listing fields that can
|
||||||
|
field_whitelist = [
|
||||||
|
'name', 'description', 'forks', 'limit', 'verbosity', 'extra_vars',
|
||||||
|
'job_tags', 'force_handlers', 'skip_tags', 'ask_variables_on_launch',
|
||||||
|
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_inventory_on_launch',
|
||||||
|
'ask_credential_on_launch', 'survey_enabled'
|
||||||
|
]
|
||||||
|
|
||||||
|
for k, v in data.items():
|
||||||
|
if hasattr(obj, k) and getattr(obj, k) != v:
|
||||||
|
if k not in field_whitelist:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def can_update_sensitive_fields(self, obj, data):
|
||||||
|
project_id = data.get('project', obj.project.id if obj.project else None)
|
||||||
|
inventory_id = data.get('inventory', obj.inventory.id if obj.inventory else None)
|
||||||
|
credential_id = data.get('credential', obj.credential.id if obj.credential else None)
|
||||||
|
cloud_credential_id = data.get('cloud_credential', obj.cloud_credential.id if obj.cloud_credential else None)
|
||||||
|
network_credential_id = data.get('network_credential', obj.network_credential.id if obj.network_credential else None)
|
||||||
|
|
||||||
|
if project_id and self.user not in Project.objects.get(pk=project_id).use_role:
|
||||||
|
return False
|
||||||
|
if inventory_id and self.user not in Inventory.objects.get(pk=inventory_id).use_role:
|
||||||
|
return False
|
||||||
|
if credential_id and self.user not in Credential.objects.get(pk=credential_id).use_role:
|
||||||
|
return False
|
||||||
|
if cloud_credential_id and self.user not in Credential.objects.get(pk=cloud_credential_id).use_role:
|
||||||
|
return False
|
||||||
|
if network_credential_id and self.user not in Credential.objects.get(pk=network_credential_id).use_role:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def can_delete(self, obj):
|
def can_delete(self, obj):
|
||||||
return self.user in obj.admin_role
|
return self.user in obj.admin_role
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from awx.main.migrations import _rbac as rbac
|
||||||
from awx.main.migrations import _ask_for_variables as ask_for_variables
|
from awx.main.migrations import _ask_for_variables as ask_for_variables
|
||||||
from awx.main.migrations import _migration_utils as migration_utils
|
from awx.main.migrations import _migration_utils as migration_utils
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
@@ -15,4 +16,5 @@ class Migration(migrations.Migration):
|
|||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
||||||
migrations.RunPython(ask_for_variables.migrate_credential),
|
migrations.RunPython(ask_for_variables.migrate_credential),
|
||||||
|
migrations.RunPython(rbac.rebuild_role_hierarchy),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ def migrate_inventory(apps, schema_editor):
|
|||||||
Inventory = apps.get_model('main', 'Inventory')
|
Inventory = apps.get_model('main', 'Inventory')
|
||||||
Permission = apps.get_model('main', 'Permission')
|
Permission = apps.get_model('main', 'Permission')
|
||||||
|
|
||||||
def role_from_permission():
|
def role_from_permission(perm):
|
||||||
if perm.permission_type == 'admin':
|
if perm.permission_type == 'admin':
|
||||||
return inventory.admin_role
|
return inventory.admin_role
|
||||||
elif perm.permission_type == 'read':
|
elif perm.permission_type == 'read':
|
||||||
@@ -233,7 +233,7 @@ def migrate_inventory(apps, schema_editor):
|
|||||||
role = None
|
role = None
|
||||||
execrole = None
|
execrole = None
|
||||||
|
|
||||||
role = role_from_permission()
|
role = role_from_permission(perm)
|
||||||
if role is None:
|
if role is None:
|
||||||
raise Exception(smart_text(u'Unhandled permission type for inventory: {}'.format( perm.permission_type)))
|
raise Exception(smart_text(u'Unhandled permission type for inventory: {}'.format( perm.permission_type)))
|
||||||
|
|
||||||
@@ -320,24 +320,30 @@ def migrate_projects(apps, schema_editor):
|
|||||||
logger.warn(smart_text(u'adding Project({}) admin: {}'.format(project.name, project.created_by.username)))
|
logger.warn(smart_text(u'adding Project({}) admin: {}'.format(project.name, project.created_by.username)))
|
||||||
|
|
||||||
for team in project.deprecated_teams.all():
|
for team in project.deprecated_teams.all():
|
||||||
team.member_role.children.add(project.use_role)
|
team.member_role.children.add(project.read_role)
|
||||||
logger.info(smart_text(u'adding Team({}) access for Project({})'.format(team.name, project.name)))
|
logger.info(smart_text(u'adding Team({}) access for Project({})'.format(team.name, project.name)))
|
||||||
|
|
||||||
if project.organization is not None:
|
|
||||||
for user in project.organization.deprecated_users.all():
|
|
||||||
project.use_role.members.add(user)
|
|
||||||
logger.info(smart_text(u'adding Organization({}) member access to Project({})'.format(project.organization.name, project.name)))
|
|
||||||
|
|
||||||
for perm in Permission.objects.filter(project=project):
|
for perm in Permission.objects.filter(project=project):
|
||||||
# All perms at this level just imply a user or team can read
|
if perm.permission_type == 'create':
|
||||||
|
role = project.use_role
|
||||||
|
else:
|
||||||
|
role = project.read_role
|
||||||
|
|
||||||
if perm.team:
|
if perm.team:
|
||||||
perm.team.member_role.children.add(project.use_role)
|
perm.team.member_role.children.add(role)
|
||||||
logger.info(smart_text(u'adding Team({}) access for Project({})'.format(perm.team.name, project.name)))
|
logger.info(smart_text(u'adding Team({}) access for Project({})'.format(perm.team.name, project.name)))
|
||||||
|
|
||||||
if perm.user:
|
if perm.user:
|
||||||
project.use_role.members.add(perm.user)
|
role.members.add(perm.user)
|
||||||
logger.info(smart_text(u'adding User({}) access for Project({})'.format(perm.user.username, project.name)))
|
logger.info(smart_text(u'adding User({}) access for Project({})'.format(perm.user.username, project.name)))
|
||||||
|
|
||||||
|
if project.organization is not None:
|
||||||
|
for user in project.organization.deprecated_users.all():
|
||||||
|
if not (project.use_role.members.filter(pk=user.id).exists() or project.admin_role.members.filter(pk=user.id).exists()):
|
||||||
|
project.read_role.members.add(user)
|
||||||
|
logger.info(smart_text(u'adding Organization({}) member access to Project({})'.format(project.organization.name, project.name)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@log_migration
|
@log_migration
|
||||||
def migrate_job_templates(apps, schema_editor):
|
def migrate_job_templates(apps, schema_editor):
|
||||||
@@ -403,7 +409,7 @@ def migrate_job_templates(apps, schema_editor):
|
|||||||
|
|
||||||
team_create_permissions = set(
|
team_create_permissions = set(
|
||||||
jt_permission_qs
|
jt_permission_qs
|
||||||
.filter(permission_type__in=['create'] if jt.job_type == 'check' else ['create'])
|
.filter(permission_type__in=['create'])
|
||||||
.values_list('team__id', flat=True)
|
.values_list('team__id', flat=True)
|
||||||
)
|
)
|
||||||
team_run_permissions = set(
|
team_run_permissions = set(
|
||||||
@@ -413,12 +419,12 @@ def migrate_job_templates(apps, schema_editor):
|
|||||||
)
|
)
|
||||||
user_create_permissions = set(
|
user_create_permissions = set(
|
||||||
jt_permission_qs
|
jt_permission_qs
|
||||||
.filter(permission_type__in=['create'] if jt.job_type == 'check' else ['run'])
|
.filter(permission_type__in=['create'])
|
||||||
.values_list('user__id', flat=True)
|
.values_list('user__id', flat=True)
|
||||||
)
|
)
|
||||||
user_run_permissions = set(
|
user_run_permissions = set(
|
||||||
jt_permission_qs
|
jt_permission_qs
|
||||||
.filter(permission_type__in=['check', 'run'] if jt.job_type == 'check' else ['create'])
|
.filter(permission_type__in=['check', 'run'] if jt.job_type == 'check' else ['run'])
|
||||||
.values_list('user__id', flat=True)
|
.values_list('user__id', flat=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -446,17 +452,20 @@ def migrate_job_templates(apps, schema_editor):
|
|||||||
logger.info(smart_text(u'transfering execute access on JobTemplate({}) to Team({})'.format(jt.name, team.name)))
|
logger.info(smart_text(u'transfering execute access on JobTemplate({}) to Team({})'.format(jt.name, team.name)))
|
||||||
|
|
||||||
for user in User.objects.filter(id__in=user_create_permissions).iterator():
|
for user in User.objects.filter(id__in=user_create_permissions).iterator():
|
||||||
|
cred = jt.credential or jt.cloud_credential
|
||||||
if (jt.inventory.id in user_inv_permissions[user.id] or
|
if (jt.inventory.id in user_inv_permissions[user.id] or
|
||||||
any([jt.inventory.id in team_inv_permissions[team.id] for team in user.deprecated_teams.all()])) and \
|
any([jt.inventory.id in team_inv_permissions[team.id] for team in user.deprecated_teams.all()])) and \
|
||||||
((not jt.credential and not jt.cloud_credential) or
|
(not cred or cred.deprecated_user == user or
|
||||||
Credential.objects.filter(Q(deprecated_user=user) | Q(deprecated_team__deprecated_users=user), jobtemplates=jt).exists()):
|
(cred.deprecated_team and cred.deprecated_team.deprecated_users.filter(pk=user.id).exists())):
|
||||||
jt.admin_role.members.add(user)
|
jt.admin_role.members.add(user)
|
||||||
logger.info(smart_text(u'transfering admin access on JobTemplate({}) to User({})'.format(jt.name, user.username)))
|
logger.info(smart_text(u'transfering admin access on JobTemplate({}) to User({})'.format(jt.name, user.username)))
|
||||||
for user in User.objects.filter(id__in=user_run_permissions).iterator():
|
for user in User.objects.filter(id__in=user_run_permissions).iterator():
|
||||||
|
cred = jt.credential or jt.cloud_credential
|
||||||
|
|
||||||
if (jt.inventory.id in user_inv_permissions[user.id] or
|
if (jt.inventory.id in user_inv_permissions[user.id] or
|
||||||
any([jt.inventory.id in team_inv_permissions[team.id] for team in user.deprecated_teams.all()])) and \
|
any([jt.inventory.id in team_inv_permissions[team.id] for team in user.deprecated_teams.all()])) and \
|
||||||
((not jt.credential and not jt.cloud_credential) or
|
(not cred or cred.deprecated_user == user or
|
||||||
Credential.objects.filter(Q(deprecated_user=user) | Q(deprecated_team__deprecated_users=user), jobtemplates=jt).exists()):
|
(cred.deprecated_team and cred.deprecated_team.deprecated_users.filter(pk=user.id).exists())):
|
||||||
jt.execute_role.members.add(user)
|
jt.execute_role.members.add(user)
|
||||||
logger.info(smart_text(u'transfering execute access on JobTemplate({}) to User({})'.format(jt.name, user.username)))
|
logger.info(smart_text(u'transfering execute access on JobTemplate({}) to User({})'.format(jt.name, user.username)))
|
||||||
|
|
||||||
@@ -468,8 +477,6 @@ def rebuild_role_hierarchy(apps, schema_editor):
|
|||||||
start = time()
|
start = time()
|
||||||
roots = Role.objects \
|
roots = Role.objects \
|
||||||
.all() \
|
.all() \
|
||||||
.exclude(pk__in=Role.parents.through.objects.all()
|
|
||||||
.values_list('from_role_id', flat=True).distinct()) \
|
|
||||||
.values_list('id', flat=True)
|
.values_list('id', flat=True)
|
||||||
stop = time()
|
stop = time()
|
||||||
logger.info('Found %d roots in %f seconds, rebuilding ancestry map' % (len(roots), stop - start))
|
logger.info('Found %d roots in %f seconds, rebuilding ancestry map' % (len(roots), stop - start))
|
||||||
|
|||||||
@@ -379,11 +379,16 @@ def activity_stream_associate(sender, instance, **kwargs):
|
|||||||
obj1 = instance
|
obj1 = instance
|
||||||
object1=camelcase_to_underscore(obj1.__class__.__name__)
|
object1=camelcase_to_underscore(obj1.__class__.__name__)
|
||||||
obj_rel = sender.__module__ + "." + sender.__name__
|
obj_rel = sender.__module__ + "." + sender.__name__
|
||||||
|
|
||||||
for entity_acted in kwargs['pk_set']:
|
for entity_acted in kwargs['pk_set']:
|
||||||
obj2 = kwargs['model']
|
obj2 = kwargs['model']
|
||||||
obj2_id = entity_acted
|
obj2_id = entity_acted
|
||||||
obj2_actual = obj2.objects.get(id=obj2_id)
|
obj2_actual = obj2.objects.get(id=obj2_id)
|
||||||
object2 = camelcase_to_underscore(obj2.__name__)
|
if isinstance(obj2_actual, Role) and obj2_actual.content_object is not None:
|
||||||
|
obj2_actual = obj2_actual.content_object
|
||||||
|
object2 = camelcase_to_underscore(obj2_actual.__class__.__name__)
|
||||||
|
else:
|
||||||
|
object2 = camelcase_to_underscore(obj2.__name__)
|
||||||
# Skip recording any inventory source, or system job template changes here.
|
# Skip recording any inventory source, or system job template changes here.
|
||||||
if isinstance(obj1, InventorySource) or isinstance(obj2_actual, InventorySource):
|
if isinstance(obj1, InventorySource) or isinstance(obj2_actual, InventorySource):
|
||||||
continue
|
continue
|
||||||
@@ -409,7 +414,7 @@ def activity_stream_associate(sender, instance, **kwargs):
|
|||||||
# If the m2m is from the User side we need to
|
# If the m2m is from the User side we need to
|
||||||
# set the content_object of the Role for our entry.
|
# set the content_object of the Role for our entry.
|
||||||
if type(instance) == User and role.content_object is not None:
|
if type(instance) == User and role.content_object is not None:
|
||||||
getattr(activity_entry, role.content_type.name).add(role.content_object)
|
getattr(activity_entry, role.content_type.name.replace(' ', '_')).add(role.content_object)
|
||||||
|
|
||||||
activity_entry.role.add(role)
|
activity_entry.role.add(role)
|
||||||
activity_entry.object_relationship_type = obj_rel
|
activity_entry.object_relationship_type = obj_rel
|
||||||
|
|||||||
@@ -441,7 +441,12 @@ class BaseTask(Task):
|
|||||||
if settings.ANSIBLE_USE_VENV:
|
if settings.ANSIBLE_USE_VENV:
|
||||||
env['VIRTUAL_ENV'] = settings.ANSIBLE_VENV_PATH
|
env['VIRTUAL_ENV'] = settings.ANSIBLE_VENV_PATH
|
||||||
env['PATH'] = os.path.join(settings.ANSIBLE_VENV_PATH, "bin") + ":" + env['PATH']
|
env['PATH'] = os.path.join(settings.ANSIBLE_VENV_PATH, "bin") + ":" + env['PATH']
|
||||||
env['PYTHONPATH'] = os.path.join(settings.ANSIBLE_VENV_PATH, "lib/python2.7/site-packages/") + ":"
|
venv_libdir = os.path.join(settings.ANSIBLE_VENV_PATH, "lib")
|
||||||
|
env.pop('PYTHONPATH', None) # default to none if no python_ver matches
|
||||||
|
for python_ver in ["python2.7", "python2.6"]:
|
||||||
|
if os.path.isdir(os.path.join(venv_libdir, python_ver)):
|
||||||
|
env['PYTHONPATH'] = os.path.join(venv_libdir, python_ver, "site-packages") + ":"
|
||||||
|
break
|
||||||
if self.should_use_proot(instance, **kwargs):
|
if self.should_use_proot(instance, **kwargs):
|
||||||
env['PROOT_TMP_DIR'] = tower_settings.AWX_PROOT_BASE_PATH
|
env['PROOT_TMP_DIR'] = tower_settings.AWX_PROOT_BASE_PATH
|
||||||
return env
|
return env
|
||||||
|
|||||||
65
awx/main/tests/factories/README.md
Normal file
65
awx/main/tests/factories/README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
factories
|
||||||
|
=========
|
||||||
|
|
||||||
|
This is a module for defining stand-alone factories and fixtures. Ideally a fixture will implement a single item.
|
||||||
|
DO NOT decorate fixtures in this module with the @pytest.fixture. These fixtures are to be combined
|
||||||
|
with fixture factories and composition using the `conftest.py` convention. Those composed fixtures
|
||||||
|
will be decorated for usage and discovery.
|
||||||
|
|
||||||
|
Use the fixtures directly in factory methods to build up the desired set of components and relationships.
|
||||||
|
Each fixture should create exactly one object and should support the option for that object to be persisted
|
||||||
|
or not.
|
||||||
|
|
||||||
|
A factory should create at a minimum a single object for that factory type. The creation of any
|
||||||
|
associated objects should be explicit. For example, the `create_organization` factory when given only
|
||||||
|
a `name` parameter will create an Organization but it will not implicitly create any other objects.
|
||||||
|
|
||||||
|
teams
|
||||||
|
-----
|
||||||
|
|
||||||
|
There is some special handling for users when adding teams. There is a short hand that allows you to
|
||||||
|
assign a user to the member\_role of a team using the string notation of `team_name:user_name`. There is
|
||||||
|
no shortcut for adding a user to the admin\_role of a team. See the roles section for more information
|
||||||
|
about how to do that.
|
||||||
|
|
||||||
|
roles
|
||||||
|
-----
|
||||||
|
|
||||||
|
The roles helper allows you pass in roles to a factory. These roles assignments will happen after
|
||||||
|
the objects are created. Using the roles parameter required that persisted=True (default).
|
||||||
|
|
||||||
|
You can use a string notation of `object_name.role_name:user` OR `object_name.role_name:object_name.child_role`
|
||||||
|
|
||||||
|
obj.parent_role:user # This will make the user a member of parent_role
|
||||||
|
obj1.role:obj2.role # This will make obj2 a child role of obj1
|
||||||
|
|
||||||
|
team1.admin_role:joe
|
||||||
|
team1.admin_role:project1.admin_role
|
||||||
|
|
||||||
|
examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
objects = create_organization('test-org')
|
||||||
|
assert objects.organization.name == 'test-org'
|
||||||
|
|
||||||
|
objects = create_organization('test-org', projects=['test-proj'])
|
||||||
|
assert objects.projects.test-proj.organization == objects.organization
|
||||||
|
|
||||||
|
objects = create_organization('test-org', persisted=False)
|
||||||
|
assert not objects.organization.pk
|
||||||
|
|
||||||
|
patterns
|
||||||
|
--------
|
||||||
|
|
||||||
|
`mk` functions are single object fixtures. They should create only a single object with the minimum deps.
|
||||||
|
They should also accept a `persited` flag, if they must be persisted to work, they raise an error if persisted=False
|
||||||
|
|
||||||
|
`generate` and `apply` functions are helpers that build up the various parts of a `create` functions objects. These
|
||||||
|
should be useful for more than one create function to use and should explicitly accept all of the values needed
|
||||||
|
to execute. These functions should also be robust and have very speciifc error reporting about constraints and/or
|
||||||
|
bad values.
|
||||||
|
|
||||||
|
`create` functions compose many of the `mk` and `generate` functions to make different object
|
||||||
|
factories. These functions when giving the minimum set of arguments should only produce a
|
||||||
|
single artifact (or the minimum needed for that object). These should be wrapped by discoverable
|
||||||
|
fixtures in various conftest.py files.
|
||||||
16
awx/main/tests/factories/__init__.py
Normal file
16
awx/main/tests/factories/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from .tower import (
|
||||||
|
create_organization,
|
||||||
|
create_job_template,
|
||||||
|
create_notification_template,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .exc import (
|
||||||
|
NotUnique,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'create_organization',
|
||||||
|
'create_job_template',
|
||||||
|
'create_notification_template',
|
||||||
|
'NotUnique',
|
||||||
|
]
|
||||||
5
awx/main/tests/factories/exc.py
Normal file
5
awx/main/tests/factories/exc.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class NotUnique(Exception):
|
||||||
|
def __init__(self, name, objects):
|
||||||
|
msg = '{} is not a unique key, found {}={}'.format(name, name, objects[name])
|
||||||
|
super(Exception, self).__init__(msg)
|
||||||
|
|
||||||
124
awx/main/tests/factories/fixtures.py
Normal file
124
awx/main/tests/factories/fixtures.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from awx.main.models import (
|
||||||
|
Organization,
|
||||||
|
Project,
|
||||||
|
Team,
|
||||||
|
Instance,
|
||||||
|
JobTemplate,
|
||||||
|
NotificationTemplate,
|
||||||
|
Credential,
|
||||||
|
Inventory,
|
||||||
|
Label,
|
||||||
|
)
|
||||||
|
|
||||||
|
# mk methods should create only a single object of a single type.
|
||||||
|
# they should also have the option of being persisted or not.
|
||||||
|
# if the object must be persisted an error should be raised when
|
||||||
|
# persisted=False
|
||||||
|
#
|
||||||
|
|
||||||
|
def mk_instance(persisted=True):
|
||||||
|
if not persisted:
|
||||||
|
raise RuntimeError('creating an Instance requires persisted=True')
|
||||||
|
from django.conf import settings
|
||||||
|
return Instance.objects.get_or_create(uuid=settings.SYSTEM_UUID, primary=True, hostname="instance.example.org")
|
||||||
|
|
||||||
|
|
||||||
|
def mk_organization(name, description=None, persisted=True):
|
||||||
|
description = description or '{}-description'.format(name)
|
||||||
|
org = Organization(name=name, description=description)
|
||||||
|
if persisted:
|
||||||
|
mk_instance(persisted)
|
||||||
|
org.save()
|
||||||
|
return org
|
||||||
|
|
||||||
|
|
||||||
|
def mk_label(name, organization=None, description=None, persisted=True):
|
||||||
|
description = description or '{}-description'.format(name)
|
||||||
|
label = Label(name=name, description=description)
|
||||||
|
if organization is not None:
|
||||||
|
label.organization = organization
|
||||||
|
if persisted:
|
||||||
|
label.save()
|
||||||
|
return label
|
||||||
|
|
||||||
|
|
||||||
|
def mk_team(name, organization=None, persisted=True):
|
||||||
|
team = Team(name=name)
|
||||||
|
if organization is not None:
|
||||||
|
team.organization = organization
|
||||||
|
if persisted:
|
||||||
|
mk_instance(persisted)
|
||||||
|
team.save()
|
||||||
|
return team
|
||||||
|
|
||||||
|
|
||||||
|
def mk_user(name, is_superuser=False, organization=None, team=None, persisted=True):
|
||||||
|
user = User(username=name, is_superuser=is_superuser)
|
||||||
|
if persisted:
|
||||||
|
user.save()
|
||||||
|
if organization is not None:
|
||||||
|
organization.member_role.members.add(user)
|
||||||
|
if team is not None:
|
||||||
|
team.member_role.members.add(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def mk_project(name, organization=None, description=None, persisted=True):
|
||||||
|
description = description or '{}-description'.format(name)
|
||||||
|
project = Project(name=name, description=description)
|
||||||
|
if organization is not None:
|
||||||
|
project.organization = organization
|
||||||
|
if persisted:
|
||||||
|
project.save()
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
def mk_credential(name, cloud=False, kind='ssh', persisted=True):
|
||||||
|
cred = Credential(name=name, cloud=cloud, kind=kind)
|
||||||
|
if persisted:
|
||||||
|
cred.save()
|
||||||
|
return cred
|
||||||
|
|
||||||
|
|
||||||
|
def mk_notification_template(name, notification_type='webhook', configuration=None, organization=None, persisted=True):
|
||||||
|
nt = NotificationTemplate(name=name)
|
||||||
|
nt.notification_type = notification_type
|
||||||
|
nt.notification_configuration = configuration or dict(url="http://localhost", headers={"Test": "Header"})
|
||||||
|
|
||||||
|
if organization is not None:
|
||||||
|
nt.organization = organization
|
||||||
|
if persisted:
|
||||||
|
nt.save()
|
||||||
|
return nt
|
||||||
|
|
||||||
|
|
||||||
|
def mk_inventory(name, organization=None, persisted=True):
|
||||||
|
inv = Inventory(name=name)
|
||||||
|
if organization is not None:
|
||||||
|
inv.organization = organization
|
||||||
|
if persisted:
|
||||||
|
inv.save()
|
||||||
|
return inv
|
||||||
|
|
||||||
|
|
||||||
|
def mk_job_template(name, job_type='run',
|
||||||
|
organization=None, inventory=None,
|
||||||
|
credential=None, persisted=True,
|
||||||
|
project=None):
|
||||||
|
jt = JobTemplate(name=name, job_type=job_type, playbook='mocked')
|
||||||
|
|
||||||
|
jt.inventory = inventory
|
||||||
|
if jt.inventory is None:
|
||||||
|
jt.ask_inventory_on_launch = True
|
||||||
|
|
||||||
|
jt.credential = credential
|
||||||
|
if jt.credential is None:
|
||||||
|
jt.ask_credential_on_launch = True
|
||||||
|
|
||||||
|
jt.project = project
|
||||||
|
|
||||||
|
if persisted:
|
||||||
|
jt.save()
|
||||||
|
return jt
|
||||||
313
awx/main/tests/factories/tower.py
Normal file
313
awx/main/tests/factories/tower.py
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from awx.main.models import (
|
||||||
|
Organization,
|
||||||
|
Project,
|
||||||
|
Team,
|
||||||
|
NotificationTemplate,
|
||||||
|
Credential,
|
||||||
|
Inventory,
|
||||||
|
Label,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .fixtures import (
|
||||||
|
mk_organization,
|
||||||
|
mk_team,
|
||||||
|
mk_user,
|
||||||
|
mk_job_template,
|
||||||
|
mk_credential,
|
||||||
|
mk_inventory,
|
||||||
|
mk_project,
|
||||||
|
mk_label,
|
||||||
|
mk_notification_template,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .exc import NotUnique
|
||||||
|
|
||||||
|
|
||||||
|
def generate_objects(artifacts, kwargs):
|
||||||
|
'''generate_objects takes a list of artifacts that are supported by
|
||||||
|
a create function and compares it to the kwargs passed in to the create
|
||||||
|
function. If a kwarg is found that is not in the artifacts list a RuntimeError
|
||||||
|
is raised.
|
||||||
|
'''
|
||||||
|
for k in kwargs.keys():
|
||||||
|
if k not in artifacts:
|
||||||
|
raise RuntimeError('{} is not a valid argument'.format(k))
|
||||||
|
return namedtuple("Objects", ",".join(artifacts))
|
||||||
|
|
||||||
|
def generate_role_objects(objects):
|
||||||
|
'''generate_role_objects assembles a dictionary of all possible objects by name.
|
||||||
|
It will raise an exception if any of the objects share a name due to the fact that
|
||||||
|
it is to be used with apply_roles, which expects unique object names.
|
||||||
|
|
||||||
|
roles share a common name e.g. admin_role, member_role. This ensures that the
|
||||||
|
roles short hand used for mapping Roles and Users in apply_roles will function as desired.
|
||||||
|
'''
|
||||||
|
combined_objects = {}
|
||||||
|
for o in objects:
|
||||||
|
if type(o) is dict:
|
||||||
|
for k,v in o.iteritems():
|
||||||
|
if combined_objects.get(k) is not None:
|
||||||
|
raise NotUnique(k, combined_objects)
|
||||||
|
combined_objects[k] = v
|
||||||
|
elif hasattr(o, 'name'):
|
||||||
|
if combined_objects.get(o.name) is not None:
|
||||||
|
raise NotUnique(o.name, combined_objects)
|
||||||
|
combined_objects[o.name] = o
|
||||||
|
else:
|
||||||
|
if o is not None:
|
||||||
|
raise RuntimeError('expected a list of dict or list of list, got a type {}'.format(type(o)))
|
||||||
|
return combined_objects
|
||||||
|
|
||||||
|
def apply_roles(roles, objects, persisted):
|
||||||
|
'''apply_roles evaluates a list of Role relationships represented as strings.
|
||||||
|
The format of this string is 'role:[user|role]'. When a user is provided, they will be
|
||||||
|
made a member of the role on the LHS. When a role is provided that role will be added to
|
||||||
|
the children of the role on the LHS.
|
||||||
|
|
||||||
|
This function assumes that objects is a dictionary that contains a unique set of key to value
|
||||||
|
mappings for all possible "Role objects". See the example below:
|
||||||
|
|
||||||
|
Mapping Users
|
||||||
|
-------------
|
||||||
|
roles = ['org1.admin_role:user1', 'team1.admin_role:user1']
|
||||||
|
objects = {'org1': Organization, 'team1': Team, 'user1': User]
|
||||||
|
|
||||||
|
Mapping Roles
|
||||||
|
-------------
|
||||||
|
roles = ['org1.admin_role:team1.admin_role']
|
||||||
|
objects = {'org1': Organization, 'team1': Team}
|
||||||
|
|
||||||
|
Invalid Mapping
|
||||||
|
---------------
|
||||||
|
roles = ['org1.admin_role:team1.admin_role']
|
||||||
|
objects = {'org1': Organization', 'user1': User} # Exception, no team1 entry
|
||||||
|
'''
|
||||||
|
if roles is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not persisted:
|
||||||
|
raise RuntimeError('roles can not be used when persisted=False')
|
||||||
|
|
||||||
|
for role in roles:
|
||||||
|
obj_role, sep, member_role = role.partition(':')
|
||||||
|
if not member_role:
|
||||||
|
raise RuntimeError('you must provide an assignment role, got None')
|
||||||
|
|
||||||
|
obj_str, o_role_str = obj_role.split('.')
|
||||||
|
member_str, m_sep, m_role_str = member_role.partition('.')
|
||||||
|
|
||||||
|
obj = objects[obj_str]
|
||||||
|
obj_role = getattr(obj, o_role_str)
|
||||||
|
|
||||||
|
member = objects[member_str]
|
||||||
|
if m_role_str:
|
||||||
|
if hasattr(member, m_role_str):
|
||||||
|
member_role = getattr(member, m_role_str)
|
||||||
|
obj_role.children.add(member_role)
|
||||||
|
else:
|
||||||
|
raise RuntimeError('unable to find {} role for {}'.format(m_role_str, member_str))
|
||||||
|
else:
|
||||||
|
if type(member) is User:
|
||||||
|
obj_role.members.add(member)
|
||||||
|
else:
|
||||||
|
raise RuntimeError('unable to add non-user {} for members list of {}'.format(member_str, obj_str))
|
||||||
|
|
||||||
|
def generate_users(organization, teams, superuser, persisted, **kwargs):
|
||||||
|
'''generate_users evaluates a mixed list of User objects and strings.
|
||||||
|
If a string is encountered a user with that username is created and added to the lookup dict.
|
||||||
|
If a User object is encountered the User.username is used as a key for the lookup dict.
|
||||||
|
|
||||||
|
A short hand for assigning a user to a team is available in the following format: "team_name:username".
|
||||||
|
If a string in that format is encounted an attempt to lookup the team by the key team_name from the teams
|
||||||
|
argumnent is made, a KeyError will be thrown if the team does not exist in the dict. The teams argument should
|
||||||
|
be a dict of {Team.name:Team}
|
||||||
|
'''
|
||||||
|
users = {}
|
||||||
|
key = 'superusers' if superuser else 'users'
|
||||||
|
if key in kwargs and kwargs.get(key) is not None:
|
||||||
|
for u in kwargs[key]:
|
||||||
|
if type(u) is User:
|
||||||
|
users[u.username] = u
|
||||||
|
else:
|
||||||
|
p1, sep, p2 = u.partition(':')
|
||||||
|
if p2:
|
||||||
|
t = teams[p1]
|
||||||
|
users[p2] = mk_user(p2, organization=organization, team=t, is_superuser=superuser, persisted=persisted)
|
||||||
|
else:
|
||||||
|
users[p1] = mk_user(p1, organization=organization, team=None, is_superuser=superuser, persisted=persisted)
|
||||||
|
return users
|
||||||
|
|
||||||
|
def generate_teams(organization, persisted, **kwargs):
|
||||||
|
'''generate_teams evalutes a mixed list of Team objects and strings.
|
||||||
|
If a string is encountered a team with that string name is created and added to the lookup dict.
|
||||||
|
If a Team object is encounted the Team.name is used as a key for the lookup dict.
|
||||||
|
'''
|
||||||
|
teams = {}
|
||||||
|
if 'teams' in kwargs and kwargs.get('teams') is not None:
|
||||||
|
for t in kwargs['teams']:
|
||||||
|
if type(t) is Team:
|
||||||
|
teams[t.name] = t
|
||||||
|
else:
|
||||||
|
teams[t] = mk_team(t, organization=organization, persisted=persisted)
|
||||||
|
return teams
|
||||||
|
|
||||||
|
|
||||||
|
class _Mapped(object):
|
||||||
|
'''_Mapped is a helper class that replaces spaces and dashes
|
||||||
|
in the name of an object and assigns the object as an attribute
|
||||||
|
|
||||||
|
input: {'my org': Organization}
|
||||||
|
output: instance.my_org = Organization
|
||||||
|
'''
|
||||||
|
def __init__(self, d):
|
||||||
|
self.d = d
|
||||||
|
for k,v in d.items():
|
||||||
|
k = k.replace(' ', '_')
|
||||||
|
k = k.replace('-', '_')
|
||||||
|
|
||||||
|
setattr(self, k.replace(' ','_'), v)
|
||||||
|
|
||||||
|
def all(self):
|
||||||
|
return self.d.values()
|
||||||
|
|
||||||
|
# create methods are intended to be called directly as needed
|
||||||
|
# or encapsulated by specific factory fixtures in a conftest
|
||||||
|
#
|
||||||
|
|
||||||
|
def create_job_template(name, roles=None, persisted=True, **kwargs):
|
||||||
|
Objects = generate_objects(["job_template",
|
||||||
|
"organization",
|
||||||
|
"inventory",
|
||||||
|
"project",
|
||||||
|
"credential",
|
||||||
|
"job_type",], kwargs)
|
||||||
|
|
||||||
|
org = None
|
||||||
|
proj = None
|
||||||
|
inv = None
|
||||||
|
cred = None
|
||||||
|
job_type = kwargs.get('job_type', 'run')
|
||||||
|
|
||||||
|
if 'organization' in kwargs:
|
||||||
|
org = kwargs['organization']
|
||||||
|
if type(org) is not Organization:
|
||||||
|
org = mk_organization(org, '%s-desc'.format(org), persisted=persisted)
|
||||||
|
|
||||||
|
if 'credential' in kwargs:
|
||||||
|
cred = kwargs['credential']
|
||||||
|
if type(cred) is not Credential:
|
||||||
|
cred = mk_credential(cred, persisted=persisted)
|
||||||
|
|
||||||
|
if 'project' in kwargs:
|
||||||
|
proj = kwargs['project']
|
||||||
|
if type(proj) is not Project:
|
||||||
|
proj = mk_project(proj, organization=org, persisted=persisted)
|
||||||
|
|
||||||
|
if 'inventory' in kwargs:
|
||||||
|
inv = kwargs['inventory']
|
||||||
|
if type(inv) is not Inventory:
|
||||||
|
inv = mk_inventory(inv, organization=org, persisted=persisted)
|
||||||
|
|
||||||
|
jt = mk_job_template(name, project=proj,
|
||||||
|
inventory=inv, credential=cred,
|
||||||
|
job_type=job_type, persisted=persisted)
|
||||||
|
|
||||||
|
role_objects = generate_role_objects([org, proj, inv, cred])
|
||||||
|
apply_roles(roles, role_objects, persisted)
|
||||||
|
|
||||||
|
return Objects(job_template=jt,
|
||||||
|
project=proj,
|
||||||
|
inventory=inv,
|
||||||
|
credential=cred,
|
||||||
|
job_type=job_type,
|
||||||
|
organization=org,)
|
||||||
|
|
||||||
|
def create_organization(name, roles=None, persisted=True, **kwargs):
|
||||||
|
Objects = generate_objects(["organization",
|
||||||
|
"teams", "users",
|
||||||
|
"superusers",
|
||||||
|
"projects",
|
||||||
|
"labels",
|
||||||
|
"notification_templates",
|
||||||
|
"inventories",], kwargs)
|
||||||
|
|
||||||
|
projects = {}
|
||||||
|
inventories = {}
|
||||||
|
labels = {}
|
||||||
|
notification_templates = {}
|
||||||
|
|
||||||
|
org = mk_organization(name, '%s-desc'.format(name), persisted=persisted)
|
||||||
|
|
||||||
|
if 'inventories' in kwargs:
|
||||||
|
for i in kwargs['inventories']:
|
||||||
|
if type(i) is Inventory:
|
||||||
|
inventories[i.name] = i
|
||||||
|
else:
|
||||||
|
inventories[i] = mk_inventory(i, organization=org, persisted=persisted)
|
||||||
|
|
||||||
|
if 'projects' in kwargs:
|
||||||
|
for p in kwargs['projects']:
|
||||||
|
if type(p) is Project:
|
||||||
|
projects[p.name] = p
|
||||||
|
else:
|
||||||
|
projects[p] = mk_project(p, organization=org, persisted=persisted)
|
||||||
|
|
||||||
|
teams = generate_teams(org, persisted, teams=kwargs.get('teams'))
|
||||||
|
superusers = generate_users(org, teams, True, persisted, superusers=kwargs.get('superusers'))
|
||||||
|
users = generate_users(org, teams, False, persisted, users=kwargs.get('users'))
|
||||||
|
|
||||||
|
if 'labels' in kwargs:
|
||||||
|
for l in kwargs['labels']:
|
||||||
|
if type(l) is Label:
|
||||||
|
labels[l.name] = l
|
||||||
|
else:
|
||||||
|
labels[l] = mk_label(l, organization=org, persisted=persisted)
|
||||||
|
|
||||||
|
if 'notification_templates' in kwargs:
|
||||||
|
for nt in kwargs['notification_templates']:
|
||||||
|
if type(nt) is NotificationTemplate:
|
||||||
|
notification_templates[nt.name] = nt
|
||||||
|
else:
|
||||||
|
notification_templates[nt] = mk_notification_template(nt, organization=org, persisted=persisted)
|
||||||
|
|
||||||
|
role_objects = generate_role_objects([org, superusers, users, teams, projects, labels, notification_templates])
|
||||||
|
apply_roles(roles, role_objects, persisted)
|
||||||
|
return Objects(organization=org,
|
||||||
|
superusers=_Mapped(superusers),
|
||||||
|
users=_Mapped(users),
|
||||||
|
teams=_Mapped(teams),
|
||||||
|
projects=_Mapped(projects),
|
||||||
|
labels=_Mapped(labels),
|
||||||
|
notification_templates=_Mapped(notification_templates),
|
||||||
|
inventories=_Mapped(inventories))
|
||||||
|
|
||||||
|
def create_notification_template(name, roles=None, persisted=True, **kwargs):
|
||||||
|
Objects = generate_objects(["notification_template",
|
||||||
|
"organization",
|
||||||
|
"users",
|
||||||
|
"superusers",
|
||||||
|
"teams",], kwargs)
|
||||||
|
|
||||||
|
organization = None
|
||||||
|
|
||||||
|
if 'organization' in kwargs:
|
||||||
|
org = kwargs['organization']
|
||||||
|
organization = mk_organization(org, '{}-desc'.format(org), persisted=persisted)
|
||||||
|
|
||||||
|
notification_template = mk_notification_template(name, organization=organization, persisted=persisted)
|
||||||
|
|
||||||
|
teams = generate_teams(organization, persisted, teams=kwargs.get('teams'))
|
||||||
|
superusers = generate_users(organization, teams, True, persisted, superusers=kwargs.get('superusers'))
|
||||||
|
users = generate_users(organization, teams, False, persisted, users=kwargs.get('users'))
|
||||||
|
|
||||||
|
role_objects = generate_role_objects([organization, notification_template])
|
||||||
|
apply_roles(roles, role_objects, persisted)
|
||||||
|
return Objects(notification_template=notification_template,
|
||||||
|
organization=organization,
|
||||||
|
users=_Mapped(users),
|
||||||
|
superusers=_Mapped(superusers),
|
||||||
|
teams=teams)
|
||||||
@@ -131,3 +131,24 @@ def test_stream_queryset_hides_shows_items(
|
|||||||
assert queryset.filter(host__pk=host.pk, operation='create').count() == 1
|
assert queryset.filter(host__pk=host.pk, operation='create').count() == 1
|
||||||
assert queryset.filter(team__pk=team.pk, operation='create').count() == 1
|
assert queryset.filter(team__pk=team.pk, operation='create').count() == 1
|
||||||
assert queryset.filter(notification_template__pk=notification_template.pk, operation='create').count() == 1
|
assert queryset.filter(notification_template__pk=notification_template.pk, operation='create').count() == 1
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||||
|
def test_stream_user_direct_role_updates(get, post, organization_factory):
|
||||||
|
objects = organization_factory('test_org',
|
||||||
|
superusers=['admin'],
|
||||||
|
users=['test'],
|
||||||
|
inventories=['inv1'])
|
||||||
|
|
||||||
|
url = reverse('api:user_roles_list', args=(objects.users.test.pk,))
|
||||||
|
post(url, dict(id=objects.inventories.inv1.read_role.pk), objects.superusers.admin)
|
||||||
|
|
||||||
|
activity_stream = ActivityStream.objects.filter(
|
||||||
|
inventory__pk=objects.inventories.inv1.pk,
|
||||||
|
user__pk=objects.users.test.pk,
|
||||||
|
role__pk=objects.inventories.inv1.read_role.pk).first()
|
||||||
|
url = reverse('api:activity_stream_detail', args=(activity_stream.pk,))
|
||||||
|
response = get(url, objects.users.test)
|
||||||
|
|
||||||
|
assert response.data['object1'] == 'user'
|
||||||
|
assert response.data['object2'] == 'inventory'
|
||||||
|
|||||||
148
awx/main/tests/functional/api/test_adhoc.py
Normal file
148
awx/main/tests/functional/api/test_adhoc.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import mock # noqa
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
def run_test_ad_hoc_command(self, **kwargs):
|
||||||
|
# Post to list to start a new ad hoc command.
|
||||||
|
expect = kwargs.pop('expect', 201)
|
||||||
|
url = kwargs.pop('url', reverse('api:ad_hoc_command_list'))
|
||||||
|
data = {
|
||||||
|
'inventory': self.inventory.pk,
|
||||||
|
'credential': self.credential.pk,
|
||||||
|
'module_name': 'command',
|
||||||
|
'module_args': 'uptime',
|
||||||
|
}
|
||||||
|
data.update(kwargs)
|
||||||
|
for k,v in data.items():
|
||||||
|
if v is None:
|
||||||
|
del data[k]
|
||||||
|
return self.post(url, data, expect=expect)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def post_adhoc(post, inventory, machine_credential):
|
||||||
|
def f(url, data, user, expect=201):
|
||||||
|
if not url:
|
||||||
|
url = reverse('api:ad_hoc_command_list')
|
||||||
|
|
||||||
|
if 'module_name' not in data:
|
||||||
|
data['module_name'] = 'command'
|
||||||
|
if 'module_args' not in data:
|
||||||
|
data['module_args'] = 'uptime'
|
||||||
|
if 'inventory' not in data:
|
||||||
|
data['inventory'] = inventory.id
|
||||||
|
if 'credential' not in data:
|
||||||
|
data['credential'] = machine_credential.id
|
||||||
|
|
||||||
|
for k,v in data.items():
|
||||||
|
if v is None:
|
||||||
|
del data[k]
|
||||||
|
|
||||||
|
return post(url, data, user, expect=expect)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_admin_post_ad_hoc_command_list(admin, post_adhoc, inventory, machine_credential):
|
||||||
|
res = post_adhoc(reverse('api:ad_hoc_command_list'), {}, admin, expect=201)
|
||||||
|
assert res.data['job_type'] == 'run'
|
||||||
|
assert res.data['inventory'], inventory.id
|
||||||
|
assert res.data['credential'] == machine_credential.id
|
||||||
|
assert res.data['module_name'] == 'command'
|
||||||
|
assert res.data['module_args'] == 'uptime'
|
||||||
|
assert res.data['limit'] == ''
|
||||||
|
assert res.data['forks'] == 0
|
||||||
|
assert res.data['verbosity'] == 0
|
||||||
|
assert res.data['become_enabled'] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_empty_post_403(admin, post):
|
||||||
|
post(reverse('api:ad_hoc_command_list'), {}, admin, expect=400)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_empty_put_405(admin, put):
|
||||||
|
put(reverse('api:ad_hoc_command_list'), {}, admin, expect=405)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_empty_patch_405(admin, patch):
|
||||||
|
patch(reverse('api:ad_hoc_command_list'), {}, admin, expect=405)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_empty_delete_405(admin, delete):
|
||||||
|
delete(reverse('api:ad_hoc_command_list'), admin, expect=405)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_post_ad_hoc_command_list(alice, post_adhoc, inventory, machine_credential):
|
||||||
|
inventory.adhoc_role.members.add(alice)
|
||||||
|
machine_credential.use_role.members.add(alice)
|
||||||
|
post_adhoc(reverse('api:ad_hoc_command_list'), {}, alice, expect=201)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_post_ad_hoc_command_list_xfail(alice, post_adhoc, inventory, machine_credential):
|
||||||
|
inventory.read_role.members.add(alice) # just read access? no dice.
|
||||||
|
machine_credential.use_role.members.add(alice)
|
||||||
|
post_adhoc(reverse('api:ad_hoc_command_list'), {}, alice, expect=403)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_post_ad_hoc_command_list_without_creds(alice, post_adhoc, inventory, machine_credential):
|
||||||
|
inventory.adhoc_role.members.add(alice)
|
||||||
|
post_adhoc(reverse('api:ad_hoc_command_list'), {}, alice, expect=403)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_post_ad_hoc_command_list_without_inventory(alice, post_adhoc, inventory, machine_credential):
|
||||||
|
machine_credential.use_role.members.add(alice)
|
||||||
|
post_adhoc(reverse('api:ad_hoc_command_list'), {}, alice, expect=403)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_admin_post_inventory_ad_hoc_command_list(admin, post_adhoc, inventory):
|
||||||
|
post_adhoc(reverse('api:inventory_ad_hoc_commands_list', args=(inventory.id,)), {'inventory': None}, admin, expect=201)
|
||||||
|
post_adhoc(reverse('api:inventory_ad_hoc_commands_list', args=(inventory.id,)), {}, admin, expect=201)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_inventory_ad_hoc_command_list(admin, alice, post_adhoc, get, inventory_factory, machine_credential):
|
||||||
|
inv1 = inventory_factory('inv1')
|
||||||
|
inv2 = inventory_factory('inv2')
|
||||||
|
|
||||||
|
post_adhoc(reverse('api:ad_hoc_command_list'), {'inventory': inv1.id}, admin, expect=201)
|
||||||
|
post_adhoc(reverse('api:ad_hoc_command_list'), {'inventory': inv2.id}, admin, expect=201)
|
||||||
|
res = get(reverse('api:ad_hoc_command_list'), admin, expect=200)
|
||||||
|
assert res.data['count'] == 2
|
||||||
|
res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv1.id,)), admin, expect=200)
|
||||||
|
assert res.data['count'] == 1
|
||||||
|
res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv2.id,)), admin, expect=200)
|
||||||
|
assert res.data['count'] == 1
|
||||||
|
|
||||||
|
inv1.adhoc_role.members.add(alice)
|
||||||
|
res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv1.id,)), alice, expect=200)
|
||||||
|
assert res.data['count'] == 0
|
||||||
|
|
||||||
|
machine_credential.use_role.members.add(alice)
|
||||||
|
res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv1.id,)), alice, expect=200)
|
||||||
|
assert res.data['count'] == 1
|
||||||
|
res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv2.id,)), alice, expect=403)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_bad_data1(admin, post_adhoc):
|
||||||
|
post_adhoc(reverse('api:ad_hoc_command_list'), {'module_name': 'command', 'module_args': None}, admin, expect=400)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_bad_data2(admin, post_adhoc):
|
||||||
|
post_adhoc(reverse('api:ad_hoc_command_list'), {'job_type': 'baddata'}, admin, expect=400)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_bad_data3(admin, post_adhoc):
|
||||||
|
post_adhoc(reverse('api:ad_hoc_command_list'), {'verbosity': -1}, admin, expect=400)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_bad_data4(admin, post_adhoc):
|
||||||
|
post_adhoc(reverse('api:ad_hoc_command_list'), {'forks': -1}, admin, expect=400)
|
||||||
|
|
||||||
18
awx/main/tests/functional/api/test_job_template.py
Normal file
18
awx/main/tests/functional/api/test_job_template.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_job_template_role_user(post, organization_factory, job_template_factory):
|
||||||
|
objects = organization_factory("org",
|
||||||
|
superusers=['admin'],
|
||||||
|
users=['test'])
|
||||||
|
|
||||||
|
jt_objects = job_template_factory("jt",
|
||||||
|
organization=objects.organization,
|
||||||
|
inventory='test_inv',
|
||||||
|
project='test_proj')
|
||||||
|
|
||||||
|
url = reverse('api:user_roles_list', args=(objects.users.test.pk,))
|
||||||
|
response = post(url, dict(id=jt_objects.job_template.execute_role.pk), objects.superusers.admin)
|
||||||
|
assert response.status_code == 204
|
||||||
111
awx/main/tests/functional/api/test_job_templates.py
Normal file
111
awx/main/tests/functional/api/test_job_templates.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import mock # noqa
|
||||||
|
import pytest
|
||||||
|
from awx.main.models.projects import ProjectOptions
|
||||||
|
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
@property
|
||||||
|
def project_playbooks(self):
|
||||||
|
return ['mocked.yml', 'alt-mocked.yml']
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"grant_project, grant_credential, grant_inventory, expect", [
|
||||||
|
(True, True, True, 201),
|
||||||
|
(True, True, False, 403),
|
||||||
|
(True, False, True, 403),
|
||||||
|
(False, True, True, 403),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_create(post, project, machine_credential, inventory, alice, grant_project, grant_credential, grant_inventory, expect):
|
||||||
|
if grant_project:
|
||||||
|
project.use_role.members.add(alice)
|
||||||
|
if grant_credential:
|
||||||
|
machine_credential.use_role.members.add(alice)
|
||||||
|
if grant_inventory:
|
||||||
|
inventory.use_role.members.add(alice)
|
||||||
|
|
||||||
|
post(reverse('api:job_template_list'), {
|
||||||
|
'name': 'Some name',
|
||||||
|
'project': project.id,
|
||||||
|
'credential': machine_credential.id,
|
||||||
|
'inventory': inventory.id,
|
||||||
|
'playbook': 'mocked.yml',
|
||||||
|
}, alice, expect=expect)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"grant_project, grant_credential, grant_inventory, expect", [
|
||||||
|
(True, True, True, 200),
|
||||||
|
(True, True, False, 403),
|
||||||
|
(True, False, True, 403),
|
||||||
|
(False, True, True, 403),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_edit_sensitive_fields(patch, job_template_factory, alice, grant_project, grant_credential, grant_inventory, expect):
|
||||||
|
objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred')
|
||||||
|
objs.job_template.admin_role.members.add(alice)
|
||||||
|
|
||||||
|
if grant_project:
|
||||||
|
objs.project.use_role.members.add(alice)
|
||||||
|
if grant_credential:
|
||||||
|
objs.credential.use_role.members.add(alice)
|
||||||
|
if grant_inventory:
|
||||||
|
objs.inventory.use_role.members.add(alice)
|
||||||
|
|
||||||
|
patch(reverse('api:job_template_detail', args=(objs.job_template.id,)), {
|
||||||
|
'name': 'Some name',
|
||||||
|
'project': objs.project.id,
|
||||||
|
'credential': objs.credential.id,
|
||||||
|
'inventory': objs.inventory.id,
|
||||||
|
'playbook': 'alt-mocked.yml',
|
||||||
|
}, alice, expect=expect)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
|
||||||
|
def test_edit_playbook(patch, job_template_factory, alice):
|
||||||
|
objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred')
|
||||||
|
objs.job_template.admin_role.members.add(alice)
|
||||||
|
objs.project.use_role.members.add(alice)
|
||||||
|
objs.credential.use_role.members.add(alice)
|
||||||
|
objs.inventory.use_role.members.add(alice)
|
||||||
|
|
||||||
|
patch(reverse('api:job_template_detail', args=(objs.job_template.id,)), {
|
||||||
|
'playbook': 'alt-mocked.yml',
|
||||||
|
}, alice, expect=200)
|
||||||
|
|
||||||
|
objs.inventory.use_role.members.remove(alice)
|
||||||
|
patch(reverse('api:job_template_detail', args=(objs.job_template.id,)), {
|
||||||
|
'playbook': 'mocked.yml',
|
||||||
|
}, alice, expect=403)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
|
||||||
|
def test_edit_nonsenstive(patch, job_template_factory, alice):
|
||||||
|
objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred')
|
||||||
|
jt = objs.job_template
|
||||||
|
jt.playbook = 'mocked.yml'
|
||||||
|
jt.save()
|
||||||
|
jt.admin_role.members.add(alice)
|
||||||
|
|
||||||
|
res = patch(reverse('api:job_template_detail', args=(jt.id,)), {
|
||||||
|
'name': 'updated',
|
||||||
|
'description': 'bar',
|
||||||
|
'forks': 14,
|
||||||
|
'limit': 'something',
|
||||||
|
'verbosity': 5,
|
||||||
|
'extra_vars': '--',
|
||||||
|
'job_tags': 'sometags',
|
||||||
|
'force_handlers': True,
|
||||||
|
'skip_tags': True,
|
||||||
|
'ask_variables_on_launch':True,
|
||||||
|
'ask_tags_on_launch':True,
|
||||||
|
'ask_job_type_on_launch':True,
|
||||||
|
'ask_inventory_on_launch':True,
|
||||||
|
'ask_credential_on_launch': True,
|
||||||
|
}, alice, expect=200)
|
||||||
|
print(res.data)
|
||||||
|
assert res.data['name'] == 'updated'
|
||||||
@@ -38,6 +38,12 @@ from awx.main.models.organization import (
|
|||||||
|
|
||||||
from awx.main.models.notifications import NotificationTemplate
|
from awx.main.models.notifications import NotificationTemplate
|
||||||
|
|
||||||
|
from awx.main.tests.factories import (
|
||||||
|
create_organization,
|
||||||
|
create_job_template,
|
||||||
|
create_notification_template,
|
||||||
|
)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Disable all django model signals.
|
Disable all django model signals.
|
||||||
'''
|
'''
|
||||||
@@ -147,18 +153,6 @@ def instance(settings):
|
|||||||
def organization(instance):
|
def organization(instance):
|
||||||
return Organization.objects.create(name="test-org", description="test-org-desc")
|
return Organization.objects.create(name="test-org", description="test-org-desc")
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def organization_factory(instance):
|
|
||||||
def factory(name):
|
|
||||||
try:
|
|
||||||
org = Organization.objects.get(name=name)
|
|
||||||
except Organization.DoesNotExist:
|
|
||||||
org = Organization.objects.create(name=name,
|
|
||||||
description="description for " + name,
|
|
||||||
)
|
|
||||||
return org
|
|
||||||
return factory
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def credential():
|
def credential():
|
||||||
return Credential.objects.create(kind='aws', name='test-cred')
|
return Credential.objects.create(kind='aws', name='test-cred')
|
||||||
@@ -282,24 +276,9 @@ def permissions():
|
|||||||
'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':True,},
|
'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':True,},
|
||||||
}
|
}
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def notification_template_factory(organization):
|
|
||||||
def n(name="test-notification_template"):
|
|
||||||
try:
|
|
||||||
notification_template = NotificationTemplate.objects.get(name=name)
|
|
||||||
except NotificationTemplate.DoesNotExist:
|
|
||||||
notification_template = NotificationTemplate(name=name,
|
|
||||||
organization=organization,
|
|
||||||
notification_type="webhook",
|
|
||||||
notification_configuration=dict(url="http://localhost",
|
|
||||||
headers={"Test": "Header"}))
|
|
||||||
notification_template.save()
|
|
||||||
return notification_template
|
|
||||||
return n
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def post():
|
def post():
|
||||||
def rf(url, data, user=None, middleware=None, **kwargs):
|
def rf(url, data, user=None, middleware=None, expect=None, **kwargs):
|
||||||
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
||||||
if 'format' not in kwargs:
|
if 'format' not in kwargs:
|
||||||
kwargs['format'] = 'json'
|
kwargs['format'] = 'json'
|
||||||
@@ -311,12 +290,16 @@ def post():
|
|||||||
response = view(request, *view_args, **view_kwargs)
|
response = view(request, *view_args, **view_kwargs)
|
||||||
if middleware:
|
if middleware:
|
||||||
middleware.process_response(request, response)
|
middleware.process_response(request, response)
|
||||||
|
if expect:
|
||||||
|
if response.status_code != expect:
|
||||||
|
print(response.data)
|
||||||
|
assert response.status_code == expect
|
||||||
return response
|
return response
|
||||||
return rf
|
return rf
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def get():
|
def get():
|
||||||
def rf(url, user=None, middleware=None, **kwargs):
|
def rf(url, user=None, middleware=None, expect=None, **kwargs):
|
||||||
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
||||||
if 'format' not in kwargs:
|
if 'format' not in kwargs:
|
||||||
kwargs['format'] = 'json'
|
kwargs['format'] = 'json'
|
||||||
@@ -328,12 +311,16 @@ def get():
|
|||||||
response = view(request, *view_args, **view_kwargs)
|
response = view(request, *view_args, **view_kwargs)
|
||||||
if middleware:
|
if middleware:
|
||||||
middleware.process_response(request, response)
|
middleware.process_response(request, response)
|
||||||
|
if expect:
|
||||||
|
if response.status_code != expect:
|
||||||
|
print(response.data)
|
||||||
|
assert response.status_code == expect
|
||||||
return response
|
return response
|
||||||
return rf
|
return rf
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def put():
|
def put():
|
||||||
def rf(url, data, user=None, middleware=None, **kwargs):
|
def rf(url, data, user=None, middleware=None, expect=None, **kwargs):
|
||||||
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
||||||
if 'format' not in kwargs:
|
if 'format' not in kwargs:
|
||||||
kwargs['format'] = 'json'
|
kwargs['format'] = 'json'
|
||||||
@@ -345,12 +332,16 @@ def put():
|
|||||||
response = view(request, *view_args, **view_kwargs)
|
response = view(request, *view_args, **view_kwargs)
|
||||||
if middleware:
|
if middleware:
|
||||||
middleware.process_response(request, response)
|
middleware.process_response(request, response)
|
||||||
|
if expect:
|
||||||
|
if response.status_code != expect:
|
||||||
|
print(response.data)
|
||||||
|
assert response.status_code == expect
|
||||||
return response
|
return response
|
||||||
return rf
|
return rf
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def patch():
|
def patch():
|
||||||
def rf(url, data, user=None, middleware=None, **kwargs):
|
def rf(url, data, user=None, middleware=None, expect=None, **kwargs):
|
||||||
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
||||||
if 'format' not in kwargs:
|
if 'format' not in kwargs:
|
||||||
kwargs['format'] = 'json'
|
kwargs['format'] = 'json'
|
||||||
@@ -362,12 +353,16 @@ def patch():
|
|||||||
response = view(request, *view_args, **view_kwargs)
|
response = view(request, *view_args, **view_kwargs)
|
||||||
if middleware:
|
if middleware:
|
||||||
middleware.process_response(request, response)
|
middleware.process_response(request, response)
|
||||||
|
if expect:
|
||||||
|
if response.status_code != expect:
|
||||||
|
print(response.data)
|
||||||
|
assert response.status_code == expect
|
||||||
return response
|
return response
|
||||||
return rf
|
return rf
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def delete():
|
def delete():
|
||||||
def rf(url, user=None, middleware=None, **kwargs):
|
def rf(url, user=None, middleware=None, expect=None, **kwargs):
|
||||||
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
||||||
if 'format' not in kwargs:
|
if 'format' not in kwargs:
|
||||||
kwargs['format'] = 'json'
|
kwargs['format'] = 'json'
|
||||||
@@ -379,12 +374,16 @@ def delete():
|
|||||||
response = view(request, *view_args, **view_kwargs)
|
response = view(request, *view_args, **view_kwargs)
|
||||||
if middleware:
|
if middleware:
|
||||||
middleware.process_response(request, response)
|
middleware.process_response(request, response)
|
||||||
|
if expect:
|
||||||
|
if response.status_code != expect:
|
||||||
|
print(response.data)
|
||||||
|
assert response.status_code == expect
|
||||||
return response
|
return response
|
||||||
return rf
|
return rf
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def head():
|
def head():
|
||||||
def rf(url, user=None, middleware=None, **kwargs):
|
def rf(url, user=None, middleware=None, expect=None, **kwargs):
|
||||||
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
||||||
if 'format' not in kwargs:
|
if 'format' not in kwargs:
|
||||||
kwargs['format'] = 'json'
|
kwargs['format'] = 'json'
|
||||||
@@ -396,12 +395,16 @@ def head():
|
|||||||
response = view(request, *view_args, **view_kwargs)
|
response = view(request, *view_args, **view_kwargs)
|
||||||
if middleware:
|
if middleware:
|
||||||
middleware.process_response(request, response)
|
middleware.process_response(request, response)
|
||||||
|
if expect:
|
||||||
|
if response.status_code != expect:
|
||||||
|
print(response.data)
|
||||||
|
assert response.status_code == expect
|
||||||
return response
|
return response
|
||||||
return rf
|
return rf
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def options():
|
def options():
|
||||||
def rf(url, data, user=None, middleware=None, **kwargs):
|
def rf(url, data, user=None, middleware=None, expect=None, **kwargs):
|
||||||
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
||||||
if 'format' not in kwargs:
|
if 'format' not in kwargs:
|
||||||
kwargs['format'] = 'json'
|
kwargs['format'] = 'json'
|
||||||
@@ -413,6 +416,10 @@ def options():
|
|||||||
response = view(request, *view_args, **view_kwargs)
|
response = view(request, *view_args, **view_kwargs)
|
||||||
if middleware:
|
if middleware:
|
||||||
middleware.process_response(request, response)
|
middleware.process_response(request, response)
|
||||||
|
if expect:
|
||||||
|
if response.status_code != expect:
|
||||||
|
print(response.data)
|
||||||
|
assert response.status_code == expect
|
||||||
return response
|
return response
|
||||||
return rf
|
return rf
|
||||||
|
|
||||||
@@ -474,3 +481,16 @@ def job_template_labels(organization, job_template):
|
|||||||
job_template.labels.create(name="label-2", organization=organization)
|
job_template.labels.create(name="label-2", organization=organization)
|
||||||
|
|
||||||
return job_template
|
return job_template
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def job_template_factory():
|
||||||
|
return create_job_template
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def organization_factory():
|
||||||
|
return create_organization
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def notification_template_factory():
|
||||||
|
return create_notification_template
|
||||||
|
|
||||||
|
|||||||
85
awx/main/tests/functional/test_fixture_factories.py
Normal file
85
awx/main/tests/functional/test_fixture_factories.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from awx.main.tests.factories import NotUnique
|
||||||
|
|
||||||
|
def test_roles_exc_not_persisted(organization_factory):
|
||||||
|
with pytest.raises(RuntimeError) as exc:
|
||||||
|
organization_factory('test-org', roles=['test-org.admin_role:user1'], persisted=False)
|
||||||
|
assert 'persisted=False' in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_roles_exc_bad_object(organization_factory):
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
organization_factory('test-org', roles=['test-project.admin_role:user'])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_roles_exc_not_unique(organization_factory):
|
||||||
|
with pytest.raises(NotUnique) as exc:
|
||||||
|
organization_factory('test-org', projects=['foo'], teams=['foo'], roles=['foo.admin_role:user'])
|
||||||
|
assert 'not a unique key' in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_roles_exc_not_assignment(organization_factory):
|
||||||
|
with pytest.raises(RuntimeError) as exc:
|
||||||
|
organization_factory('test-org', projects=['foo'], roles=['foo.admin_role'])
|
||||||
|
assert 'provide an assignment' in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_roles_exc_not_found(organization_factory):
|
||||||
|
with pytest.raises(RuntimeError) as exc:
|
||||||
|
organization_factory('test-org', users=['user'], projects=['foo'], roles=['foo.admin_role:user.bad_role'])
|
||||||
|
assert 'unable to find' in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_roles_exc_not_user(organization_factory):
|
||||||
|
with pytest.raises(RuntimeError) as exc:
|
||||||
|
organization_factory('test-org', projects=['foo'], roles=['foo.admin_role:foo'])
|
||||||
|
assert 'unable to add non-user' in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_org_factory_roles(organization_factory):
|
||||||
|
objects = organization_factory('org_roles_test',
|
||||||
|
teams=['team1', 'team2'],
|
||||||
|
users=['team1:foo', 'bar'],
|
||||||
|
projects=['baz', 'bang'],
|
||||||
|
roles=['team2.member_role:foo',
|
||||||
|
'team1.admin_role:bar',
|
||||||
|
'team1.admin_role:team2.admin_role',
|
||||||
|
'baz.admin_role:foo'])
|
||||||
|
|
||||||
|
assert objects.users.bar in objects.teams.team2.admin_role
|
||||||
|
assert objects.users.foo in objects.projects.baz.admin_role
|
||||||
|
assert objects.users.foo in objects.teams.team1.member_role
|
||||||
|
assert objects.teams.team2.admin_role in objects.teams.team1.admin_role.children.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_org_factory(organization_factory):
|
||||||
|
objects = organization_factory('organization1',
|
||||||
|
teams=['team1'],
|
||||||
|
superusers=['superuser'],
|
||||||
|
users=['admin', 'alice', 'team1:bob'],
|
||||||
|
projects=['proj1'])
|
||||||
|
assert hasattr(objects.users, 'admin')
|
||||||
|
assert hasattr(objects.users, 'alice')
|
||||||
|
assert hasattr(objects.superusers, 'superuser')
|
||||||
|
assert objects.users.bob in objects.teams.team1.member_role.members.all()
|
||||||
|
assert objects.projects.proj1.organization == objects.organization
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_job_template_factory(job_template_factory):
|
||||||
|
jt_objects = job_template_factory('testJT', organization='org1',
|
||||||
|
project='proj1', inventory='inventory1',
|
||||||
|
credential='cred1')
|
||||||
|
assert jt_objects.job_template.name == 'testJT'
|
||||||
|
assert jt_objects.project.name == 'proj1'
|
||||||
|
assert jt_objects.inventory.name == 'inventory1'
|
||||||
|
assert jt_objects.credential.name == 'cred1'
|
||||||
|
assert jt_objects.inventory.organization.name == 'org1'
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import mock # noqa
|
import mock # noqa
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django.db import transaction
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from awx.main.models import Project
|
from awx.main.models import Project
|
||||||
|
|
||||||
@@ -9,62 +8,55 @@ from awx.main.models import Project
|
|||||||
#
|
#
|
||||||
# Project listing and visibility tests
|
# Project listing and visibility tests
|
||||||
#
|
#
|
||||||
|
@pytest.fixture
|
||||||
|
def team_project_list(organization_factory):
|
||||||
|
objects = organization_factory('org-test',
|
||||||
|
superusers=['admin'],
|
||||||
|
users=['team1:alice', 'team2:bob'],
|
||||||
|
teams=['team1', 'team2'],
|
||||||
|
projects=['pteam1', 'pteam2', 'pshared'],
|
||||||
|
roles=['team1.member_role:pteam1.admin_role',
|
||||||
|
'team2.member_role:pteam2.admin_role',
|
||||||
|
'team1.member_role:pshared.admin_role',
|
||||||
|
'team2.member_role:pshared.admin_role'])
|
||||||
|
return objects
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_user_project_list(get, project_factory, organization, admin, alice, bob):
|
def test_user_project_list(get, organization_factory):
|
||||||
'List of projects a user has access to, filtered by projects you can also see'
|
'List of projects a user has access to, filtered by projects you can also see'
|
||||||
|
|
||||||
organization.member_role.members.add(alice, bob)
|
objects = organization_factory('org1',
|
||||||
|
projects=['alice project', 'bob project', 'shared project'],
|
||||||
|
superusers=['admin'],
|
||||||
|
users=['alice', 'bob'],
|
||||||
|
roles=['alice project.admin_role:alice',
|
||||||
|
'bob project.admin_role:bob',
|
||||||
|
'shared project.admin_role:bob',
|
||||||
|
'shared project.admin_role:alice'])
|
||||||
|
|
||||||
alice_project = project_factory('alice project')
|
assert get(reverse('api:user_projects_list', args=(objects.superusers.admin.pk,)), objects.superusers.admin).data['count'] == 3
|
||||||
alice_project.admin_role.members.add(alice)
|
|
||||||
|
|
||||||
bob_project = project_factory('bob project')
|
|
||||||
bob_project.admin_role.members.add(bob)
|
|
||||||
|
|
||||||
shared_project = project_factory('shared project')
|
|
||||||
shared_project.admin_role.members.add(alice)
|
|
||||||
shared_project.admin_role.members.add(bob)
|
|
||||||
|
|
||||||
# admins can see all projects
|
|
||||||
assert get(reverse('api:user_projects_list', args=(admin.pk,)), admin).data['count'] == 3
|
|
||||||
|
|
||||||
# admins can see everyones projects
|
# admins can see everyones projects
|
||||||
assert get(reverse('api:user_projects_list', args=(alice.pk,)), admin).data['count'] == 2
|
assert get(reverse('api:user_projects_list', args=(objects.users.alice.pk,)), objects.superusers.admin).data['count'] == 2
|
||||||
assert get(reverse('api:user_projects_list', args=(bob.pk,)), admin).data['count'] == 2
|
assert get(reverse('api:user_projects_list', args=(objects.users.bob.pk,)), objects.superusers.admin).data['count'] == 2
|
||||||
|
|
||||||
# users can see their own projects
|
# users can see their own projects
|
||||||
assert get(reverse('api:user_projects_list', args=(alice.pk,)), alice).data['count'] == 2
|
assert get(reverse('api:user_projects_list', args=(objects.users.alice.pk,)), objects.users.alice).data['count'] == 2
|
||||||
|
|
||||||
# alice should only be able to see the shared project when looking at bobs projects
|
# alice should only be able to see the shared project when looking at bobs projects
|
||||||
assert get(reverse('api:user_projects_list', args=(bob.pk,)), alice).data['count'] == 1
|
assert get(reverse('api:user_projects_list', args=(objects.users.bob.pk,)), objects.users.alice).data['count'] == 1
|
||||||
|
|
||||||
# alice should see all projects they can see when viewing an admin
|
# alice should see all projects they can see when viewing an admin
|
||||||
assert get(reverse('api:user_projects_list', args=(admin.pk,)), alice).data['count'] == 2
|
assert get(reverse('api:user_projects_list', args=(objects.superusers.admin.pk,)), objects.users.alice).data['count'] == 2
|
||||||
|
|
||||||
|
|
||||||
def setup_test_team_project_list(project_factory, team_factory, admin, alice, bob):
|
|
||||||
team1 = team_factory('team1')
|
|
||||||
team2 = team_factory('team2')
|
|
||||||
|
|
||||||
team1_project = project_factory('team1 project')
|
|
||||||
team1_project.admin_role.parents.add(team1.member_role)
|
|
||||||
|
|
||||||
team2_project = project_factory('team2 project')
|
|
||||||
team2_project.admin_role.parents.add(team2.member_role)
|
|
||||||
|
|
||||||
shared_project = project_factory('shared project')
|
|
||||||
shared_project.admin_role.parents.add(team1.member_role)
|
|
||||||
shared_project.admin_role.parents.add(team2.member_role)
|
|
||||||
|
|
||||||
team1.member_role.members.add(alice)
|
|
||||||
team2.member_role.members.add(bob)
|
|
||||||
return team1, team2
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_team_project_list(get, project_factory, team_factory, admin, alice, bob):
|
def test_team_project_list(get, team_project_list):
|
||||||
'List of projects a team has access to, filtered by projects you can also see'
|
objects = team_project_list
|
||||||
team1, team2 = setup_test_team_project_list(project_factory, team_factory, admin, alice, bob)
|
|
||||||
|
team1, team2 = objects.teams.team1, objects.teams.team2
|
||||||
|
alice, bob, admin = objects.users.alice, objects.users.bob, objects.superusers.admin
|
||||||
|
|
||||||
# admins can see all projects on a team
|
# admins can see all projects on a team
|
||||||
assert get(reverse('api:team_projects_list', args=(team1.pk,)), admin).data['count'] == 2
|
assert get(reverse('api:team_projects_list', args=(team1.pk,)), admin).data['count'] == 2
|
||||||
@@ -78,12 +70,6 @@ def test_team_project_list(get, project_factory, team_factory, admin, alice, bob
|
|||||||
assert get(reverse('api:team_projects_list', args=(team2.pk,)), alice).data['count'] == 1
|
assert get(reverse('api:team_projects_list', args=(team2.pk,)), alice).data['count'] == 1
|
||||||
team2.read_role.members.remove(alice)
|
team2.read_role.members.remove(alice)
|
||||||
|
|
||||||
# Test user endpoints first, very similar tests to test_user_project_list
|
|
||||||
# but permissions are being derived from team membership instead.
|
|
||||||
with transaction.atomic():
|
|
||||||
res = get(reverse('api:user_projects_list', args=(bob.pk,)), alice)
|
|
||||||
assert res.status_code == 403
|
|
||||||
|
|
||||||
# admins can see all projects
|
# admins can see all projects
|
||||||
assert get(reverse('api:user_projects_list', args=(admin.pk,)), admin).data['count'] == 3
|
assert get(reverse('api:user_projects_list', args=(admin.pk,)), admin).data['count'] == 3
|
||||||
|
|
||||||
@@ -98,17 +84,11 @@ def test_team_project_list(get, project_factory, team_factory, admin, alice, bob
|
|||||||
assert get(reverse('api:user_projects_list', args=(admin.pk,)), alice).data['count'] == 2
|
assert get(reverse('api:user_projects_list', args=(admin.pk,)), alice).data['count'] == 2
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_team_project_list_fail1(get, project_factory, team_factory, admin, alice, bob):
|
def test_team_project_list_fail1(get, team_project_list):
|
||||||
# alice should not be able to see team2 projects because she doesn't have access to team2
|
objects = team_project_list
|
||||||
team1, team2 = setup_test_team_project_list(project_factory, team_factory, admin, alice, bob)
|
res = get(reverse('api:team_projects_list', args=(objects.teams.team2.pk,)), objects.users.alice)
|
||||||
res = get(reverse('api:team_projects_list', args=(team2.pk,)), alice)
|
|
||||||
assert res.status_code == 403
|
assert res.status_code == 403
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_team_project_list_fail2(get, project_factory, team_factory, admin, alice, bob):
|
|
||||||
team1, team2 = setup_test_team_project_list(project_factory, team_factory, admin, alice, bob)
|
|
||||||
# alice should not be able to see bob
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("u,expected_status_code", [
|
@pytest.mark.parametrize("u,expected_status_code", [
|
||||||
('rando', 403),
|
('rando', 403),
|
||||||
('org_member', 403),
|
('org_member', 403),
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ from awx.main.access import (
|
|||||||
)
|
)
|
||||||
from awx.main.migrations import _rbac as rbac
|
from awx.main.migrations import _rbac as rbac
|
||||||
from awx.main.models import Permission
|
from awx.main.models import Permission
|
||||||
|
from awx.main.models.jobs import JobTemplate
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_job_template_migration_check(credential, deploy_jobtemplate, check_jobtemplate, user):
|
def test_job_template_migration_check(credential, deploy_jobtemplate, check_jobtemplate, user):
|
||||||
@@ -155,3 +158,26 @@ def test_job_template_access_superuser(check_license, user, deploy_jobtemplate):
|
|||||||
# THEN all access checks should pass
|
# THEN all access checks should pass
|
||||||
assert access.can_read(deploy_jobtemplate)
|
assert access.can_read(deploy_jobtemplate)
|
||||||
assert access.can_add({})
|
assert access.can_add({})
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.job_permissions
|
||||||
|
def test_job_template_creator_access(project, rando, post):
|
||||||
|
|
||||||
|
project.admin_role.members.add(rando)
|
||||||
|
with mock.patch(
|
||||||
|
'awx.main.models.projects.ProjectOptions.playbooks',
|
||||||
|
new_callable=mock.PropertyMock(return_value=['helloworld.yml'])):
|
||||||
|
response = post(reverse('api:job_template_list', args=[]), dict(
|
||||||
|
name='newly-created-jt',
|
||||||
|
job_type='run',
|
||||||
|
ask_inventory_on_launch=True,
|
||||||
|
ask_credential_on_launch=True,
|
||||||
|
project=project.pk,
|
||||||
|
playbook='helloworld.yml'
|
||||||
|
), rando)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
jt_pk = response.data['id']
|
||||||
|
jt_obj = JobTemplate.objects.get(pk=jt_pk)
|
||||||
|
# Creating a JT should place the creator in the admin role
|
||||||
|
assert rando in jt_obj.admin_role
|
||||||
|
|||||||
@@ -31,20 +31,22 @@ def test_label_access_superuser(label, user):
|
|||||||
assert access.can_delete(label)
|
assert access.can_delete(label)
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_label_access_admin(label, user, organization_factory):
|
def test_label_access_admin(organization_factory):
|
||||||
'''can_change because I am an admin of that org'''
|
'''can_change because I am an admin of that org'''
|
||||||
a = user('admin', False)
|
no_members = organization_factory("no_members")
|
||||||
org_no_members = organization_factory("no_members")
|
members = organization_factory("has_members",
|
||||||
org_members = organization_factory("has_members")
|
users=['admin'],
|
||||||
|
labels=['test'])
|
||||||
|
|
||||||
label.organization.admin_role.members.add(a)
|
label = members.labels.test
|
||||||
org_members.admin_role.members.add(a)
|
admin = members.users.admin
|
||||||
|
members.organization.admin_role.members.add(admin)
|
||||||
|
|
||||||
access = LabelAccess(user('admin', False))
|
access = LabelAccess(admin)
|
||||||
assert not access.can_change(label, {'organization': org_no_members.id})
|
assert not access.can_change(label, {'organization': no_members.organization.id})
|
||||||
assert access.can_read(label)
|
assert access.can_read(label)
|
||||||
assert access.can_change(label, None)
|
assert access.can_change(label, None)
|
||||||
assert access.can_change(label, {'organization': org_members.id})
|
assert access.can_change(label, {'organization': members.organization.id})
|
||||||
assert access.can_delete(label)
|
assert access.can_delete(label)
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -25,35 +25,44 @@ def test_notification_template_get_queryset_orgadmin(notification_template, user
|
|||||||
assert access.get_queryset().count() == 1
|
assert access.get_queryset().count() == 1
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_notification_template_access_superuser(notification_template, user, notification_template_factory):
|
def test_notification_template_access_superuser(notification_template_factory):
|
||||||
access = NotificationTemplateAccess(user('admin', True))
|
nf_objects = notification_template_factory('test-orphaned', organization='test', superusers=['admin'])
|
||||||
assert access.can_read(notification_template)
|
admin = nf_objects.superusers.admin
|
||||||
assert access.can_change(notification_template, None)
|
nf = nf_objects.notification_template
|
||||||
assert access.can_delete(notification_template)
|
|
||||||
nf = notification_template_factory("test-orphaned")
|
access = NotificationTemplateAccess(admin)
|
||||||
|
assert access.can_read(nf)
|
||||||
|
assert access.can_change(nf, None)
|
||||||
|
assert access.can_delete(nf)
|
||||||
|
|
||||||
nf.organization = None
|
nf.organization = None
|
||||||
nf.save()
|
nf.save()
|
||||||
|
|
||||||
assert access.can_read(nf)
|
assert access.can_read(nf)
|
||||||
assert access.can_change(nf, None)
|
assert access.can_change(nf, None)
|
||||||
assert access.can_delete(nf)
|
assert access.can_delete(nf)
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_notification_template_access_admin(notification_template, user, organization_factory, notification_template_factory):
|
def test_notification_template_access_admin(organization_factory, notification_template_factory):
|
||||||
adm = user('admin', False)
|
other_objects = organization_factory('other')
|
||||||
other_org = organization_factory('other')
|
present_objects = organization_factory('present',
|
||||||
present_org = organization_factory('present')
|
users=['admin'],
|
||||||
notification_template.organization.admin_role.members.add(adm)
|
notification_templates=['test-notification'],
|
||||||
present_org.admin_role.members.add(adm)
|
roles=['present.admin_role:admin'])
|
||||||
|
|
||||||
access = NotificationTemplateAccess(user('admin', False))
|
notification_template = present_objects.notification_templates.test_notification
|
||||||
|
other_org = other_objects.organization
|
||||||
|
present_org = present_objects.organization
|
||||||
|
admin = present_objects.users.admin
|
||||||
|
|
||||||
|
access = NotificationTemplateAccess(admin)
|
||||||
assert not access.can_change(notification_template, {'organization': other_org.id})
|
assert not access.can_change(notification_template, {'organization': other_org.id})
|
||||||
assert access.can_read(notification_template)
|
assert access.can_read(notification_template)
|
||||||
assert access.can_change(notification_template, None)
|
assert access.can_change(notification_template, None)
|
||||||
assert access.can_change(notification_template, {'organization': present_org.id})
|
assert access.can_change(notification_template, {'organization': present_org.id})
|
||||||
assert access.can_delete(notification_template)
|
assert access.can_delete(notification_template)
|
||||||
|
|
||||||
nf = notification_template_factory("test-orphaned")
|
nf = notification_template_factory("test-orphaned")
|
||||||
nf.organization = None
|
|
||||||
nf.save()
|
|
||||||
assert not access.can_read(nf)
|
assert not access.can_read(nf)
|
||||||
assert not access.can_change(nf, None)
|
assert not access.can_change(nf, None)
|
||||||
assert not access.can_delete(nf)
|
assert not access.can_delete(nf)
|
||||||
|
|||||||
@@ -404,164 +404,6 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
|||||||
del data[k]
|
del data[k]
|
||||||
return self.post(url, data, expect=expect)
|
return self.post(url, data, expect=expect)
|
||||||
|
|
||||||
@mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock)
|
|
||||||
def test_ad_hoc_command_list(self, ignore):
|
|
||||||
url = reverse('api:ad_hoc_command_list')
|
|
||||||
|
|
||||||
# Retrieve the empty list of ad hoc commands.
|
|
||||||
qs = AdHocCommand.objects.none()
|
|
||||||
self.check_get_list(url, 'admin', qs)
|
|
||||||
self.check_get_list(url, 'normal', qs)
|
|
||||||
self.check_get_list(url, 'other', qs)
|
|
||||||
self.check_get_list(url, 'nobody', qs)
|
|
||||||
self.check_get_list(url, None, qs, expect=401)
|
|
||||||
|
|
||||||
# Start a new ad hoc command. Only admin and normal user (org admin)
|
|
||||||
# can run commands by default.
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.run_test_ad_hoc_command()
|
|
||||||
self.assertEqual(response['job_type'], 'run')
|
|
||||||
self.assertEqual(response['inventory'], self.inventory.pk)
|
|
||||||
self.assertEqual(response['credential'], self.credential.pk)
|
|
||||||
self.assertEqual(response['module_name'], 'command')
|
|
||||||
self.assertEqual(response['module_args'], 'uptime')
|
|
||||||
self.assertEqual(response['limit'], '')
|
|
||||||
self.assertEqual(response['forks'], 0)
|
|
||||||
self.assertEqual(response['verbosity'], 0)
|
|
||||||
self.assertEqual(response['become_enabled'], False)
|
|
||||||
self.put(url, {}, expect=405)
|
|
||||||
self.patch(url, {}, expect=405)
|
|
||||||
self.delete(url, expect=405)
|
|
||||||
with self.current_user('normal'):
|
|
||||||
self.run_test_ad_hoc_command()
|
|
||||||
self.put(url, {}, expect=405)
|
|
||||||
self.patch(url, {}, expect=405)
|
|
||||||
self.delete(url, expect=405)
|
|
||||||
with self.current_user('other'):
|
|
||||||
self.run_test_ad_hoc_command(expect=403)
|
|
||||||
self.put(url, {}, expect=405)
|
|
||||||
self.patch(url, {}, expect=405)
|
|
||||||
self.delete(url, expect=405)
|
|
||||||
with self.current_user('nobody'):
|
|
||||||
self.run_test_ad_hoc_command(expect=403)
|
|
||||||
self.put(url, {}, expect=405)
|
|
||||||
self.patch(url, {}, expect=405)
|
|
||||||
self.delete(url, expect=405)
|
|
||||||
with self.current_user(None):
|
|
||||||
self.run_test_ad_hoc_command(expect=401)
|
|
||||||
self.put(url, {}, expect=401)
|
|
||||||
self.patch(url, {}, expect=401)
|
|
||||||
self.delete(url, expect=401)
|
|
||||||
|
|
||||||
# Retrieve the list of ad hoc commands (only admin/normal can see by default).
|
|
||||||
qs = AdHocCommand.objects.all()
|
|
||||||
self.assertEqual(qs.count(), 2)
|
|
||||||
self.check_get_list(url, 'admin', qs)
|
|
||||||
self.check_get_list(url, 'normal', qs)
|
|
||||||
qs = AdHocCommand.objects.none()
|
|
||||||
self.check_get_list(url, 'other', qs)
|
|
||||||
self.check_get_list(url, 'nobody', qs)
|
|
||||||
self.check_get_list(url, None, qs, expect=401)
|
|
||||||
|
|
||||||
# Explicitly give other user updater permission on the inventory (still
|
|
||||||
# not allowed to run ad hoc commands).
|
|
||||||
user_roles_list_url = reverse('api:user_roles_list', args=(self.other_django_user.pk,))
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.post(user_roles_list_url, {"id": self.inventory.update_role.id}, expect=204)
|
|
||||||
with self.current_user('other'):
|
|
||||||
self.run_test_ad_hoc_command(expect=403)
|
|
||||||
self.check_get_list(url, 'other', qs)
|
|
||||||
|
|
||||||
# Add executor role permissions to other. Fails
|
|
||||||
# when other user can't read credential.
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.post(user_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204)
|
|
||||||
with self.current_user('other'):
|
|
||||||
self.run_test_ad_hoc_command(expect=403)
|
|
||||||
|
|
||||||
# Succeeds once other user has a readable credential. Other user can
|
|
||||||
# only see his own ad hoc command (because of credential permissions).
|
|
||||||
other_cred = self.create_test_credential(user=self.other_django_user)
|
|
||||||
with self.current_user('other'):
|
|
||||||
self.run_test_ad_hoc_command(credential=other_cred.pk)
|
|
||||||
qs = AdHocCommand.objects.filter(created_by=self.other_django_user)
|
|
||||||
self.assertEqual(qs.count(), 1)
|
|
||||||
self.check_get_list(url, 'other', qs)
|
|
||||||
|
|
||||||
# Explicitly give nobody user read permission on the inventory.
|
|
||||||
nobody_roles_list_url = reverse('api:user_roles_list', args=(self.nobody_django_user.pk,))
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.post(nobody_roles_list_url, {"id": self.inventory.read_role.id}, expect=204)
|
|
||||||
with self.current_user('nobody'):
|
|
||||||
self.run_test_ad_hoc_command(credential=other_cred.pk, expect=403)
|
|
||||||
self.check_get_list(url, 'other', qs)
|
|
||||||
|
|
||||||
# Create a cred for the nobody user, run an ad hoc command as the admin
|
|
||||||
# user with that cred. Nobody user can still not see the ad hoc command
|
|
||||||
# without the run_ad_hoc_commands permission flag.
|
|
||||||
nobody_cred = self.create_test_credential(user=self.nobody_django_user)
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(credential=nobody_cred.pk)
|
|
||||||
qs = AdHocCommand.objects.none()
|
|
||||||
self.check_get_list(url, 'nobody', qs)
|
|
||||||
|
|
||||||
# Give the nobody user the run_ad_hoc_commands flag, and can now see
|
|
||||||
# the one ad hoc command previously run.
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.post(nobody_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204)
|
|
||||||
qs = AdHocCommand.objects.filter(credential_id=nobody_cred.pk)
|
|
||||||
self.assertEqual(qs.count(), 1)
|
|
||||||
self.check_get_list(url, 'nobody', qs)
|
|
||||||
|
|
||||||
# Post without inventory (should fail).
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(inventory=None, expect=400)
|
|
||||||
|
|
||||||
# Post without credential (should fail).
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(credential=None, expect=400)
|
|
||||||
|
|
||||||
# Post with empty or unsupported module name (empty defaults to command).
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.run_test_ad_hoc_command(module_name=None)
|
|
||||||
self.assertEqual(response['module_name'], 'command')
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.run_test_ad_hoc_command(module_name='')
|
|
||||||
self.assertEqual(response['module_name'], 'command')
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(module_name='transcombobulator', expect=400)
|
|
||||||
|
|
||||||
# Post with empty module args for shell/command modules (should fail),
|
|
||||||
# empty args for other modules ok.
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(module_args=None, expect=400)
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(module_name='shell', module_args=None, expect=400)
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(module_name='shell', module_args='', expect=400)
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(module_name='ping', module_args=None)
|
|
||||||
|
|
||||||
# Post with invalid values for other parameters.
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(job_type='something', expect=400)
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.run_test_ad_hoc_command(job_type='check')
|
|
||||||
self.assertEqual(response['job_type'], 'check')
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(verbosity=-1, expect=400)
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(forks=-1, expect=400)
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.run_test_ad_hoc_command(become_enabled=True)
|
|
||||||
self.assertEqual(response['become_enabled'], True)
|
|
||||||
|
|
||||||
# Try to run with expired license.
|
|
||||||
self.create_expired_license_file()
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(expect=403)
|
|
||||||
with self.current_user('normal'):
|
|
||||||
self.run_test_ad_hoc_command(expect=403)
|
|
||||||
|
|
||||||
@mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock)
|
@mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock)
|
||||||
def test_ad_hoc_command_detail(self, ignore):
|
def test_ad_hoc_command_detail(self, ignore):
|
||||||
@@ -953,98 +795,6 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
|||||||
self.patch(url, {}, expect=401)
|
self.patch(url, {}, expect=401)
|
||||||
self.delete(url, expect=401)
|
self.delete(url, expect=401)
|
||||||
|
|
||||||
@mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock)
|
|
||||||
def test_inventory_ad_hoc_commands_list(self, ignore):
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.run_test_ad_hoc_command()
|
|
||||||
response = self.run_test_ad_hoc_command(inventory=self.inventory2.pk)
|
|
||||||
|
|
||||||
# Test the ad hoc commands list for an inventory. Should only return
|
|
||||||
# the ad hoc command(s) run against that inventory. Posting should
|
|
||||||
# start a new ad hoc command and always set the inventory from the URL.
|
|
||||||
url = reverse('api:inventory_ad_hoc_commands_list', args=(self.inventory.pk,))
|
|
||||||
inventory_url = reverse('api:inventory_detail', args=(self.inventory.pk,))
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.get(url, expect=200)
|
|
||||||
self.assertEqual(response['count'], 1)
|
|
||||||
response = self.run_test_ad_hoc_command(url=url, inventory=None, expect=201)
|
|
||||||
self.assertEqual(response['inventory'], self.inventory.pk)
|
|
||||||
response = self.run_test_ad_hoc_command(url=url, inventory=self.inventory2.pk, expect=201)
|
|
||||||
self.assertEqual(response['inventory'], self.inventory.pk)
|
|
||||||
self.put(url, {}, expect=405)
|
|
||||||
self.patch(url, {}, expect=405)
|
|
||||||
self.delete(url, expect=405)
|
|
||||||
response = self.get(inventory_url, expect=200)
|
|
||||||
self.assertTrue(response['can_run_ad_hoc_commands'])
|
|
||||||
with self.current_user('normal'):
|
|
||||||
response = self.get(url, expect=200)
|
|
||||||
self.assertEqual(response['count'], 3)
|
|
||||||
response = self.run_test_ad_hoc_command(url=url, inventory=None, expect=201)
|
|
||||||
self.assertEqual(response['inventory'], self.inventory.pk)
|
|
||||||
self.put(url, {}, expect=405)
|
|
||||||
self.patch(url, {}, expect=405)
|
|
||||||
self.delete(url, expect=405)
|
|
||||||
response = self.get(inventory_url, expect=200)
|
|
||||||
self.assertTrue(response['can_run_ad_hoc_commands'])
|
|
||||||
with self.current_user('other'):
|
|
||||||
self.get(url, expect=403)
|
|
||||||
self.post(url, {}, expect=403)
|
|
||||||
self.put(url, {}, expect=405)
|
|
||||||
self.patch(url, {}, expect=405)
|
|
||||||
self.delete(url, expect=405)
|
|
||||||
with self.current_user('nobody'):
|
|
||||||
self.get(url, expect=403)
|
|
||||||
self.post(url, {}, expect=403)
|
|
||||||
self.put(url, {}, expect=405)
|
|
||||||
self.patch(url, {}, expect=405)
|
|
||||||
self.delete(url, expect=405)
|
|
||||||
with self.current_user(None):
|
|
||||||
self.get(url, expect=401)
|
|
||||||
self.post(url, {}, expect=401)
|
|
||||||
self.put(url, {}, expect=401)
|
|
||||||
self.patch(url, {}, expect=401)
|
|
||||||
self.delete(url, expect=401)
|
|
||||||
|
|
||||||
# Create another unrelated inventory permission with run_ad_hoc_commands
|
|
||||||
# set; this tests an edge case in the RBAC query where we'll return
|
|
||||||
# can_run_ad_hoc_commands = True when we shouldn't.
|
|
||||||
nobody_roles_list_url = reverse('api:user_roles_list', args=(self.nobody_django_user.pk,))
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.post(nobody_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204)
|
|
||||||
|
|
||||||
# Create a credential for the other user and explicitly give other
|
|
||||||
# user admin permission on the inventory (still not allowed to run ad
|
|
||||||
# hoc commands; can get the list but can't see any items).
|
|
||||||
other_cred = self.create_test_credential(user=self.other_django_user)
|
|
||||||
user_roles_list_url = reverse('api:user_roles_list', args=(self.other_django_user.pk,))
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.post(user_roles_list_url, {"id": self.inventory.update_role.id}, expect=204)
|
|
||||||
with self.current_user('other'):
|
|
||||||
response = self.get(url, expect=200)
|
|
||||||
self.assertEqual(response['count'], 0)
|
|
||||||
response = self.get(inventory_url, expect=200)
|
|
||||||
self.assertFalse(response['can_run_ad_hoc_commands'])
|
|
||||||
self.run_test_ad_hoc_command(url=url, inventory=None, credential=other_cred.pk, expect=403)
|
|
||||||
|
|
||||||
# Update permission to allow other user to run ad hoc commands. Can
|
|
||||||
# only see his own ad hoc commands (because of credential permission).
|
|
||||||
with self.current_user('admin'):
|
|
||||||
response = self.post(user_roles_list_url, {"id": self.inventory.adhoc_role.id}, expect=204)
|
|
||||||
with self.current_user('other'):
|
|
||||||
response = self.get(url, expect=200)
|
|
||||||
self.assertEqual(response['count'], 0)
|
|
||||||
self.run_test_ad_hoc_command(url=url, inventory=None, credential=other_cred.pk, expect=201)
|
|
||||||
response = self.get(url, expect=200)
|
|
||||||
self.assertEqual(response['count'], 1)
|
|
||||||
response = self.get(inventory_url, expect=200)
|
|
||||||
self.assertTrue(response['can_run_ad_hoc_commands'])
|
|
||||||
|
|
||||||
# Try to run with expired license.
|
|
||||||
self.create_expired_license_file()
|
|
||||||
with self.current_user('admin'):
|
|
||||||
self.run_test_ad_hoc_command(url=url, expect=403)
|
|
||||||
with self.current_user('normal'):
|
|
||||||
self.run_test_ad_hoc_command(url=url, expect=403)
|
|
||||||
|
|
||||||
def test_host_ad_hoc_commands_list(self):
|
def test_host_ad_hoc_commands_list(self):
|
||||||
# TODO: Figure out why this test needs pexpect
|
# TODO: Figure out why this test needs pexpect
|
||||||
|
|||||||
@@ -1770,6 +1770,7 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
|||||||
self.assertFalse(inventory_update.name.endswith(inventory_update.inventory_source.name), inventory_update.name)
|
self.assertFalse(inventory_update.name.endswith(inventory_update.inventory_source.name), inventory_update.name)
|
||||||
|
|
||||||
def test_update_from_rax(self):
|
def test_update_from_rax(self):
|
||||||
|
self.skipTest('Skipping until we can resolve the CERTIFICATE_VERIFY_FAILED issue: #1706')
|
||||||
source_username = getattr(settings, 'TEST_RACKSPACE_USERNAME', '')
|
source_username = getattr(settings, 'TEST_RACKSPACE_USERNAME', '')
|
||||||
source_password = getattr(settings, 'TEST_RACKSPACE_API_KEY', '')
|
source_password = getattr(settings, 'TEST_RACKSPACE_API_KEY', '')
|
||||||
source_regions = getattr(settings, 'TEST_RACKSPACE_REGIONS', '')
|
source_regions = getattr(settings, 'TEST_RACKSPACE_REGIONS', '')
|
||||||
|
|||||||
@@ -961,7 +961,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
|||||||
self.assertEqual(jobs_qs.count(), 7)
|
self.assertEqual(jobs_qs.count(), 7)
|
||||||
job = jobs_qs[0]
|
job = jobs_qs[0]
|
||||||
self.assertEqual(job.launch_type, 'callback')
|
self.assertEqual(job.launch_type, 'callback')
|
||||||
self.assertEqual(job.limit, ':&'.join([job_template.limit, host.name]))
|
self.assertEqual(job.limit, host.name)
|
||||||
self.assertEqual(job.hosts.count(), 1)
|
self.assertEqual(job.hosts.count(), 1)
|
||||||
self.assertEqual(job.hosts.all()[0], host)
|
self.assertEqual(job.hosts.all()[0], host)
|
||||||
|
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ body .navbar {
|
|||||||
border-color: #E8E8E8;
|
border-color: #E8E8E8;
|
||||||
}
|
}
|
||||||
body .navbar .navbar-brand {
|
body .navbar .navbar-brand {
|
||||||
color: #848992;
|
color: #707070;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
body .navbar .navbar-brand:focus,
|
body .navbar .navbar-brand:focus,
|
||||||
body .navbar .navbar-brand:hover {
|
body .navbar .navbar-brand:hover {
|
||||||
color: #848992;
|
color: #707070;
|
||||||
}
|
}
|
||||||
body .navbar .navbar-brand img {
|
body .navbar .navbar-brand img {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -60,7 +60,7 @@ body .navbar .navbar-brand > span {
|
|||||||
body .navbar .navbar-title {
|
body .navbar .navbar-title {
|
||||||
float: left;
|
float: left;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
color: #848992;
|
color: #707070;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -74,19 +74,19 @@ body.show-title .navbar .navbar-title {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
body .navbar .navbar-nav > li > a {
|
body .navbar .navbar-nav > li > a {
|
||||||
color: #848992;
|
color: #707070;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
body .navbar .navbar-nav > li > a:focus,
|
body .navbar .navbar-nav > li > a:focus,
|
||||||
body .navbar .navbar-nav > li > a:hover {
|
body .navbar .navbar-nav > li > a:hover {
|
||||||
color: #848992;
|
color: #707070;
|
||||||
}
|
}
|
||||||
body .navbar .navbar-nav > li > a > span.glyphicon {
|
body .navbar .navbar-nav > li > a > span.glyphicon {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
color: #B7B7B7;
|
color: #848992;
|
||||||
}
|
}
|
||||||
|
|
||||||
body .page-header {
|
body .page-header {
|
||||||
@@ -110,7 +110,7 @@ body .description .hide-description span.glyphicon {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
body .description .hide-description:hover span.glyphicon {
|
body .description .hide-description:hover span.glyphicon {
|
||||||
color: #B7B7B7;
|
color: #848992;
|
||||||
}
|
}
|
||||||
body ul.breadcrumb,
|
body ul.breadcrumb,
|
||||||
body .description,
|
body .description,
|
||||||
@@ -167,7 +167,7 @@ body .form-actions button {
|
|||||||
body .form-horizontal .control-label {
|
body .form-horizontal .control-label {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: #848992;
|
color: #707070;
|
||||||
}
|
}
|
||||||
body textarea.form-control {
|
body textarea.form-control {
|
||||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
@@ -182,22 +182,22 @@ body .description pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body .tooltip.bottom .tooltip-arrow {
|
body .tooltip.bottom .tooltip-arrow {
|
||||||
border-bottom-color: #848992;
|
border-bottom-color: #707070;
|
||||||
}
|
}
|
||||||
body .tooltip.top .tooltip-arrow {
|
body .tooltip.top .tooltip-arrow {
|
||||||
border-top-color: #848992;
|
border-top-color: #707070;
|
||||||
}
|
}
|
||||||
body .tooltip.left .tooltip-arrow {
|
body .tooltip.left .tooltip-arrow {
|
||||||
border-left-color: #848992;
|
border-left-color: #707070;
|
||||||
}
|
}
|
||||||
body .tooltip.right .tooltip-arrow {
|
body .tooltip.right .tooltip-arrow {
|
||||||
border-right-color: #848992;
|
border-right-color: #707070;
|
||||||
}
|
}
|
||||||
body .tooltip.in {
|
body .tooltip.in {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
body .tooltip-inner {
|
body .tooltip-inner {
|
||||||
background-color: #848992;
|
background-color: #707070;
|
||||||
}
|
}
|
||||||
|
|
||||||
body .btn {
|
body .btn {
|
||||||
@@ -205,7 +205,7 @@ body .btn {
|
|||||||
}
|
}
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: #FFFFFF;
|
background-color: #FFFFFF;
|
||||||
color: #848992;
|
color: #707070;
|
||||||
border: 1px solid #E8E8E8;
|
border: 1px solid #E8E8E8;
|
||||||
}
|
}
|
||||||
.btn-primary:hover,
|
.btn-primary:hover,
|
||||||
@@ -224,7 +224,7 @@ body .btn {
|
|||||||
.open>.dropdown-toggle.btn-primary:hover,
|
.open>.dropdown-toggle.btn-primary:hover,
|
||||||
.open>.dropdown-toggle.btn-primary {
|
.open>.dropdown-toggle.btn-primary {
|
||||||
background-color: #FAFAFA;
|
background-color: #FAFAFA;
|
||||||
color: #848992;
|
color: #707070;
|
||||||
border: 1px solid #E8E8E8;
|
border: 1px solid #E8E8E8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ body #footer {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
color: #848992;
|
color: #707070;
|
||||||
}
|
}
|
||||||
body #footer .footer-logo {
|
body #footer .footer-logo {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -302,7 +302,7 @@ body #footer .footer-copyright {
|
|||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
body #footer .footer-copyright a {
|
body #footer .footer-copyright a {
|
||||||
color: #848992;
|
color: #707070;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
@@ -329,7 +329,7 @@ body #footer .footer-copyright a {
|
|||||||
border-color: #E8E8E8;
|
border-color: #E8E8E8;
|
||||||
}
|
}
|
||||||
body .navbar .navbar-toggle .icon-bar {
|
body .navbar .navbar-toggle .icon-bar {
|
||||||
background-color: #B7B7B7;
|
background-color: #848992;
|
||||||
}
|
}
|
||||||
body .navbar .tooltip {
|
body .navbar .tooltip {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 15 KiB |
@@ -962,6 +962,10 @@ input[type="checkbox"].checkbox-no-label {
|
|||||||
.checkbox-inline, .radio-inline {
|
.checkbox-inline, .radio-inline {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox-inline.stack-inline {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-options {
|
.checkbox-options {
|
||||||
|
|||||||
@@ -178,7 +178,7 @@
|
|||||||
|
|
||||||
.Form-formGroup--checkbox{
|
.Form-formGroup--checkbox{
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Form-subForm {
|
.Form-subForm {
|
||||||
@@ -324,6 +324,12 @@
|
|||||||
|
|
||||||
.select2-dropdown{
|
.select2-dropdown{
|
||||||
border:1px solid @field-border;
|
border:1px solid @field-border;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--open .select2-dropdown--below {
|
||||||
|
margin-top: -1px;
|
||||||
|
border-top: 1px solid @field-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Form-dropDown:focus{
|
.Form-dropDown:focus{
|
||||||
|
|||||||
@@ -214,7 +214,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#job-detail-container {
|
#job-detail-container {
|
||||||
|
|
||||||
.well {
|
.well {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -276,6 +276,8 @@
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: @white;
|
background-color: @white;
|
||||||
|
min-height: 40px;
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
border-top: 1px solid @grey;
|
border-top: 1px solid @grey;
|
||||||
}
|
}
|
||||||
@@ -318,7 +320,7 @@
|
|||||||
|
|
||||||
#play-section {
|
#play-section {
|
||||||
.table-detail {
|
.table-detail {
|
||||||
height: 150px;
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +423,6 @@
|
|||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
#hosts-table-detail {
|
#hosts-table-detail {
|
||||||
height: 150px;
|
|
||||||
background-color: @white;
|
background-color: @white;
|
||||||
}
|
}
|
||||||
#hosts-table-detail table {
|
#hosts-table-detail table {
|
||||||
|
|||||||
32
awx/ui/client/lib/lrInfiniteScroll/.bower.json
Normal file
32
awx/ui/client/lib/lrInfiniteScroll/.bower.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "lrInfiniteScroll",
|
||||||
|
"main": "lrInfiniteScroll.js",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"homepage": "https://github.com/lorenzofox3/lrInfiniteScroll",
|
||||||
|
"authors": [
|
||||||
|
"lorenzofox3 <laurent34azerty@gmail.com>"
|
||||||
|
],
|
||||||
|
"description": "angular directive to handle element scroll",
|
||||||
|
"keywords": [
|
||||||
|
"angular",
|
||||||
|
"scroll",
|
||||||
|
"inifinite"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"ignore": [
|
||||||
|
"**/.*",
|
||||||
|
"node_modules",
|
||||||
|
"bower_components",
|
||||||
|
"test",
|
||||||
|
"tests"
|
||||||
|
],
|
||||||
|
"_release": "1.0.0",
|
||||||
|
"_resolution": {
|
||||||
|
"type": "version",
|
||||||
|
"tag": "1.0.0",
|
||||||
|
"commit": "c833e9d8ff56d6c66e2a21ed7f27ad840f159a8b"
|
||||||
|
},
|
||||||
|
"_source": "https://github.com/lorenzofox3/lrInfiniteScroll.git",
|
||||||
|
"_target": "~1.0.0",
|
||||||
|
"_originalSource": "lrInfiniteScroll"
|
||||||
|
}
|
||||||
2
awx/ui/client/lib/lrInfiniteScroll/index.js
Normal file
2
awx/ui/client/lib/lrInfiniteScroll/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
require('./lrInfiniteScroll');
|
||||||
|
module.exports = 'lrInfiniteScroll';
|
||||||
@@ -2,13 +2,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
var module = ng.module('lrInfiniteScroll', []);
|
var module = ng.module('lrInfiniteScroll', []);
|
||||||
|
|
||||||
module.directive('lrInfiniteScroll', ['$log', '$timeout', function ($log, timeout) {
|
module.directive('lrInfiniteScroll', ['$timeout', function (timeout) {
|
||||||
return{
|
return{
|
||||||
link: function (scope, element, attr) {
|
link: function (scope, element, attr) {
|
||||||
var
|
var
|
||||||
lengthThreshold = attr.scrollThreshold || 50,
|
lengthThreshold = attr.scrollThreshold || 50,
|
||||||
timeThreshold = attr.timeThreshold || 400,
|
timeThreshold = attr.timeThreshold || 400,
|
||||||
direction = attr.direction || 'down',
|
|
||||||
handler = scope.$eval(attr.lrInfiniteScroll),
|
handler = scope.$eval(attr.lrInfiniteScroll),
|
||||||
promise = null,
|
promise = null,
|
||||||
lastRemaining = 9999;
|
lastRemaining = 9999;
|
||||||
@@ -20,14 +19,14 @@
|
|||||||
handler = ng.noop;
|
handler = ng.noop;
|
||||||
}
|
}
|
||||||
|
|
||||||
$log.debug('lrInfiniteScroll: ' + attr.lrInfiniteScroll);
|
|
||||||
|
|
||||||
element.bind('scroll', function () {
|
element.bind('scroll', function () {
|
||||||
var remaining = (direction === 'down') ? element[0].scrollHeight - (element[0].clientHeight + element[0].scrollTop) : element[0].scrollTop;
|
var
|
||||||
// if we have reached the threshold and we scroll down
|
remaining = element[0].scrollHeight - (element[0].clientHeight + element[0].scrollTop);
|
||||||
if ((direction === 'down' && remaining < lengthThreshold && (remaining - lastRemaining) < 0) ||
|
|
||||||
direction === 'up' && remaining < lengthThreshold) {
|
//if we have reached the threshold and we scroll down
|
||||||
//if there is already a timer running which has not expired yet we have to cancel it and restart the timer
|
if (remaining < lengthThreshold && (remaining - lastRemaining) < 0) {
|
||||||
|
|
||||||
|
//if there is already a timer running which has no expired yet we have to cancel it and restart the timer
|
||||||
if (promise !== null) {
|
if (promise !== null) {
|
||||||
timeout.cancel(promise);
|
timeout.cancel(promise);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
multiSelectExtended: true,
|
multiSelectExtended: true,
|
||||||
index: false,
|
index: false,
|
||||||
hover: true,
|
hover: true,
|
||||||
|
emptyListText : 'No Teams exist',
|
||||||
fields: {
|
fields: {
|
||||||
name: {
|
name: {
|
||||||
key: true,
|
key: true,
|
||||||
label: 'name'
|
label: 'name'
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
multiSelectExtended: true,
|
multiSelectExtended: true,
|
||||||
index: false,
|
index: false,
|
||||||
hover: true,
|
hover: true,
|
||||||
|
emptyListText : 'No Users exist',
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
first_name: {
|
first_name: {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default
|
|||||||
return i.role;
|
return i.role;
|
||||||
}))
|
}))
|
||||||
.filter((role) => {
|
.filter((role) => {
|
||||||
return !!attrs.teamRoleList == !!role.team_id;
|
return Boolean(attrs.teamRoleList) === Boolean(role.team_id);
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.name
|
if (a.name
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ __deferLoadIfEnabled();
|
|||||||
|
|
||||||
var tower = angular.module('Tower', [
|
var tower = angular.module('Tower', [
|
||||||
//'ngAnimate',
|
//'ngAnimate',
|
||||||
|
'lrInfiniteScroll',
|
||||||
'ngSanitize',
|
'ngSanitize',
|
||||||
'ngCookies',
|
'ngCookies',
|
||||||
about.name,
|
about.name,
|
||||||
@@ -269,7 +270,7 @@ var tower = angular.module('Tower', [
|
|||||||
}).
|
}).
|
||||||
|
|
||||||
state('projects', {
|
state('projects', {
|
||||||
url: '/projects',
|
url: '/projects?{status}',
|
||||||
templateUrl: urlPrefix + 'partials/projects.html',
|
templateUrl: urlPrefix + 'partials/projects.html',
|
||||||
controller: ProjectsList,
|
controller: ProjectsList,
|
||||||
data: {
|
data: {
|
||||||
@@ -297,6 +298,10 @@ var tower = angular.module('Tower', [
|
|||||||
controller: ProjectsEdit,
|
controller: ProjectsEdit,
|
||||||
data: {
|
data: {
|
||||||
activityStreamId: 'id'
|
activityStreamId: 'id'
|
||||||
|
},
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
parent: 'projects',
|
||||||
|
label: 'EDIT PROJECT'
|
||||||
}
|
}
|
||||||
}).
|
}).
|
||||||
state('projectOrganizations', {
|
state('projectOrganizations', {
|
||||||
@@ -340,6 +345,10 @@ var tower = angular.module('Tower', [
|
|||||||
controller: TeamsEdit,
|
controller: TeamsEdit,
|
||||||
data: {
|
data: {
|
||||||
activityStreamId: 'team_id'
|
activityStreamId: 'team_id'
|
||||||
|
},
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
parent: "teams",
|
||||||
|
label: "EDIT TEAM"
|
||||||
}
|
}
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default
|
|||||||
var licenseInfo = FeaturesService.getLicenseInfo();
|
var licenseInfo = FeaturesService.getLicenseInfo();
|
||||||
scope.licenseType = licenseInfo ? licenseInfo.license_type : null;
|
scope.licenseType = licenseInfo ? licenseInfo.license_type : null;
|
||||||
if (!licenseInfo) {
|
if (!licenseInfo) {
|
||||||
console.warn("License info not loaded correctly");
|
console.warn("License info not loaded correctly"); // jshint ignore:line
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (response) {
|
.catch(function (response) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
|
|||||||
Wait('start');
|
Wait('start');
|
||||||
|
|
||||||
var list = ProjectList,
|
var list = ProjectList,
|
||||||
defaultUrl = GetBasePath('projects'),
|
defaultUrl = GetBasePath('projects') + ($stateParams.status ? '?status=' + $stateParams.status : ''),
|
||||||
view = GenerateList,
|
view = GenerateList,
|
||||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||||
mode = (base === 'projects') ? 'edit' : 'select',
|
mode = (base === 'projects') ? 'edit' : 'select',
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log,
|
|||||||
generator.reset();
|
generator.reset();
|
||||||
|
|
||||||
$scope.user_type_options = user_type_options;
|
$scope.user_type_options = user_type_options;
|
||||||
$scope.user_type = user_type_options[0]
|
$scope.user_type = user_type_options[0];
|
||||||
$scope.$watch('user_type', user_type_sync($scope));
|
$scope.$watch('user_type', user_type_sync($scope));
|
||||||
|
|
||||||
CreateSelect2({
|
CreateSelect2({
|
||||||
@@ -271,7 +271,7 @@ export function UsersEdit($scope, $rootScope, $location,
|
|||||||
generator.reset();
|
generator.reset();
|
||||||
|
|
||||||
$scope.user_type_options = user_type_options;
|
$scope.user_type_options = user_type_options;
|
||||||
$scope.user_type = user_type_options[0]
|
$scope.user_type = user_type_options[0];
|
||||||
$scope.$watch('user_type', user_type_sync($scope));
|
$scope.$watch('user_type', user_type_sync($scope));
|
||||||
|
|
||||||
var setScopeFields = function(data){
|
var setScopeFields = function(data){
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default
|
|||||||
label: "Inventories",
|
label: "Inventories",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "/#/inventories/?inventory_sources_with_failures",
|
url: "/#/inventories?status=sync-failed",
|
||||||
number: scope.data.inventories.inventory_failed,
|
number: scope.data.inventories.inventory_failed,
|
||||||
label: "Inventory Sync Failures",
|
label: "Inventory Sync Failures",
|
||||||
isFailureCount: true
|
isFailureCount: true
|
||||||
@@ -60,7 +60,7 @@ export default
|
|||||||
label: "Projects"
|
label: "Projects"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "/#/projects/?status=failed",
|
url: "/#/projects?status=failed",
|
||||||
number: scope.data.projects.failed,
|
number: scope.data.projects.failed,
|
||||||
label: "Project Sync Failures",
|
label: "Project Sync Failures",
|
||||||
isFailureCount: true
|
isFailureCount: true
|
||||||
|
|||||||
@@ -118,6 +118,7 @@
|
|||||||
top: auto;
|
top: auto;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DashboardGraphs-periodDropdown,
|
.DashboardGraphs-periodDropdown,
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ export default
|
|||||||
rows: 10,
|
rows: 10,
|
||||||
awPopOver: "SSH key description",
|
awPopOver: "SSH key description",
|
||||||
awPopOverWatch: "key_description",
|
awPopOverWatch: "key_description",
|
||||||
dataTitle: 'Help',
|
dataTitle: 'Private Key',
|
||||||
dataPlacement: 'right',
|
dataPlacement: 'right',
|
||||||
dataContainer: "body",
|
dataContainer: "body",
|
||||||
subForm: "credentialSubForm"
|
subForm: "credentialSubForm"
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export default
|
|||||||
},
|
},
|
||||||
job_tags: {
|
job_tags: {
|
||||||
label: 'Job Tags',
|
label: 'Job Tags',
|
||||||
type: 'textarea',
|
type: 'text',
|
||||||
rows: 1,
|
rows: 1,
|
||||||
addRequired: false,
|
addRequired: false,
|
||||||
editRequired: false,
|
editRequired: false,
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
|||||||
dataTitle: 'SCM Clean',
|
dataTitle: 'SCM Clean',
|
||||||
dataContainer: 'body',
|
dataContainer: 'body',
|
||||||
dataPlacement: 'right',
|
dataPlacement: 'right',
|
||||||
labelClass: 'checkbox-options'
|
labelClass: 'checkbox-options stack-inline'
|
||||||
}, {
|
}, {
|
||||||
name: 'scm_delete_on_update',
|
name: 'scm_delete_on_update',
|
||||||
label: 'Delete on Update',
|
label: 'Delete on Update',
|
||||||
@@ -186,7 +186,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
|||||||
dataTitle: 'SCM Delete',
|
dataTitle: 'SCM Delete',
|
||||||
dataContainer: 'body',
|
dataContainer: 'body',
|
||||||
dataPlacement: 'right',
|
dataPlacement: 'right',
|
||||||
labelClass: 'checkbox-options'
|
labelClass: 'checkbox-options stack-inline'
|
||||||
}, {
|
}, {
|
||||||
name: 'scm_update_on_launch',
|
name: 'scm_update_on_launch',
|
||||||
label: 'Update on Launch',
|
label: 'Update on Launch',
|
||||||
@@ -197,7 +197,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
|||||||
dataTitle: 'SCM Update',
|
dataTitle: 'SCM Update',
|
||||||
dataContainer: 'body',
|
dataContainer: 'body',
|
||||||
dataPlacement: 'right',
|
dataPlacement: 'right',
|
||||||
labelClass: 'checkbox-options'
|
labelClass: 'checkbox-options stack-inline'
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
scm_update_cache_timeout: {
|
scm_update_cache_timeout: {
|
||||||
@@ -273,7 +273,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
include: "NotificationsList"
|
include: "NotificationsList",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -132,9 +132,10 @@ export default
|
|||||||
"delete": {
|
"delete": {
|
||||||
label: 'Remove',
|
label: 'Remove',
|
||||||
ngClick: 'deletePermissionFromTeam(team_id, team_obj.name, role.name, role.summary_fields.resource_name, role.related.teams)',
|
ngClick: 'deletePermissionFromTeam(team_id, team_obj.name, role.name, role.summary_fields.resource_name, role.related.teams)',
|
||||||
class: "List-actionButton--delete",
|
'class': "List-actionButton--delete",
|
||||||
iconClass: 'fa fa-times',
|
iconClass: 'fa fa-times',
|
||||||
awToolTip: 'Dissasociate permission from team'
|
awToolTip: 'Dissasociate permission from team',
|
||||||
|
dataPlacement: 'top'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideOnSuperuser: true
|
hideOnSuperuser: true
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export default
|
|||||||
iterator: 'team',
|
iterator: 'team',
|
||||||
open: false,
|
open: false,
|
||||||
index: false,
|
index: false,
|
||||||
|
suppressEmptyText: true,
|
||||||
|
|
||||||
actions: {},
|
actions: {},
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default
|
|||||||
angular.module('SchedulesHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', 'SearchHelper', 'PaginationHelpers', listGenerator.name, 'ModalDialog',
|
angular.module('SchedulesHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', 'SearchHelper', 'PaginationHelpers', listGenerator.name, 'ModalDialog',
|
||||||
'GeneratorHelpers'])
|
'GeneratorHelpers'])
|
||||||
|
|
||||||
.factory('EditSchedule', ['SchedulerInit', '$rootScope', 'Wait', 'Rest',
|
.factory('EditSchedule', ['SchedulerInit', '$rootScope', 'Wait', 'Rest',
|
||||||
'ProcessErrors', 'GetBasePath', 'SchedulePost', '$state',
|
'ProcessErrors', 'GetBasePath', 'SchedulePost', '$state',
|
||||||
function(SchedulerInit, $rootScope, Wait, Rest, ProcessErrors,
|
function(SchedulerInit, $rootScope, Wait, Rest, ProcessErrors,
|
||||||
GetBasePath, SchedulePost, $state) {
|
GetBasePath, SchedulePost, $state) {
|
||||||
@@ -176,7 +176,7 @@ export default
|
|||||||
};
|
};
|
||||||
}])
|
}])
|
||||||
|
|
||||||
.factory('AddSchedule', ['$location', '$rootScope', '$stateParams',
|
.factory('AddSchedule', ['$location', '$rootScope', '$stateParams',
|
||||||
'SchedulerInit', 'Wait', 'GetBasePath', 'Empty', 'SchedulePost', '$state', 'Rest', 'ProcessErrors',
|
'SchedulerInit', 'Wait', 'GetBasePath', 'Empty', 'SchedulePost', '$state', 'Rest', 'ProcessErrors',
|
||||||
function($location, $rootScope, $stateParams, SchedulerInit,
|
function($location, $rootScope, $stateParams, SchedulerInit,
|
||||||
Wait, GetBasePath, Empty, SchedulePost, $state, Rest,
|
Wait, GetBasePath, Empty, SchedulePost, $state, Rest,
|
||||||
@@ -295,8 +295,7 @@ export default
|
|||||||
}])
|
}])
|
||||||
|
|
||||||
.factory('SchedulePost', ['Rest', 'ProcessErrors', 'RRuleToAPI', 'Wait',
|
.factory('SchedulePost', ['Rest', 'ProcessErrors', 'RRuleToAPI', 'Wait',
|
||||||
'ToJSON',
|
function(Rest, ProcessErrors, RRuleToAPI, Wait) {
|
||||||
function(Rest, ProcessErrors, RRuleToAPI, Wait, ToJSON) {
|
|
||||||
return function(params) {
|
return function(params) {
|
||||||
var scope = params.scope,
|
var scope = params.scope,
|
||||||
url = params.url,
|
url = params.url,
|
||||||
@@ -326,8 +325,8 @@ export default
|
|||||||
schedule.extra_data = JSON.stringify(extra_vars);
|
schedule.extra_data = JSON.stringify(extra_vars);
|
||||||
}
|
}
|
||||||
else if(scope.extraVars){
|
else if(scope.extraVars){
|
||||||
schedule.extra_data = scope.parseType === 'yaml' ?
|
schedule.extra_data = scope.parseType === 'yaml' ?
|
||||||
(scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars;
|
(scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars;
|
||||||
}
|
}
|
||||||
Rest.setUrl(url);
|
Rest.setUrl(url);
|
||||||
if (mode === 'add') {
|
if (mode === 'add') {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function InventoriesList($scope, $rootScope, $location, $log,
|
|||||||
Find, Empty, $state) {
|
Find, Empty, $state) {
|
||||||
|
|
||||||
var list = InventoryList,
|
var list = InventoryList,
|
||||||
defaultUrl = GetBasePath('inventory'),
|
defaultUrl = GetBasePath('inventory') + ($stateParams.status === 'sync-failed' ? '?not__inventory_sources_with_failures=0' : ''),
|
||||||
view = generateList,
|
view = generateList,
|
||||||
paths = $location.path().replace(/^\//, '').split('/'),
|
paths = $location.path().replace(/^\//, '').split('/'),
|
||||||
mode = (paths[0] === 'inventories') ? 'edit' : 'select';
|
mode = (paths[0] === 'inventories') ? 'edit' : 'select';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import InventoriesList from './inventory-list.controller';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'inventories',
|
name: 'inventories',
|
||||||
route: '/inventories',
|
route: '/inventories?{status}',
|
||||||
templateUrl: templateUrl('inventories/inventories'),
|
templateUrl: templateUrl('inventories/inventories'),
|
||||||
controller: InventoriesList,
|
controller: InventoriesList,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
group_id: id
|
group_id: id
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
$scope.showFailedHosts = function(x, y, z){
|
$scope.showFailedHosts = function() {
|
||||||
$state.go('inventoryManage', {failed: true}, {reload: true});
|
$state.go('inventoryManage', {failed: true}, {reload: true});
|
||||||
};
|
};
|
||||||
$scope.scheduleGroup = function(id) {
|
$scope.scheduleGroup = function(id) {
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
$scope.$parent.groupsSelected = selection.length > 0 ? true : false;
|
$scope.$parent.groupsSelected = selection.length > 0 ? true : false;
|
||||||
$scope.$parent.groupsSelectedItems = selection.selectedItems;
|
$scope.$parent.groupsSelectedItems = selection.selectedItems;
|
||||||
});
|
});
|
||||||
$scope.$on('PostRefresh', () =>{
|
$scope.$on('PostRefresh', () => {
|
||||||
$scope.groups.forEach( (group, index) => {
|
$scope.groups.forEach( (group, index) => {
|
||||||
var group_status, hosts_status;
|
var group_status, hosts_status;
|
||||||
group_status = GetSyncStatusMsg({
|
group_status = GetSyncStatusMsg({
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {ManageHostsAdd, ManageHostsEdit} from './hosts.route';
|
|||||||
|
|
||||||
export default
|
export default
|
||||||
angular.module('manageHosts', [])
|
angular.module('manageHosts', [])
|
||||||
.run(['$stateExtender', '$state', function($stateExtender, $state){
|
.run(['$stateExtender', function($stateExtender){
|
||||||
$stateExtender.addState(ManageHostsAdd);
|
$stateExtender.addState(ManageHostsAdd);
|
||||||
$stateExtender.addState(ManageHostsEdit);
|
$stateExtender.addState(ManageHostsEdit);
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default
|
|||||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$scope.canEdit = data['script'] !== null;
|
$scope.canEdit = data.script !== null;
|
||||||
if (!$scope.canEdit) {
|
if (!$scope.canEdit) {
|
||||||
$scope.script = "Script contents hidden";
|
$scope.script = "Script contents hidden";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hosts-summary-table" class="table-detail" lr-infinite-scroll="getNextPage" scroll-threshold="10" time-threshold="500" ng-hide="hosts.length == 0">
|
<div id="hosts-summary-table" class="table-detail" lr-infinite-scroll="getNextPage" scroll-threshold="10" time-threshold="500">
|
||||||
<table class="table">
|
<table class="table" ng-class="{'JobDetails-table--noResults': hosts.length === 0}">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="List-tableRow" ng-repeat="host in hosts track by $index" id="{{ host.id }}" ng-class-even="'List-tableRow--evenRow'" ng-class-odd="'List-tableRow--oddRow'">
|
<tr class="List-tableRow" ng-repeat="host in hosts track by $index" id="{{ host.id }}" ng-class-even="'List-tableRow--evenRow'" ng-class-odd="'List-tableRow--oddRow'">
|
||||||
<td class="List-tableCell name col-lg-6 col-md-6 col-sm-6 col-xs-6">
|
<td class="List-tableCell name col-lg-6 col-md-6 col-sm-6 col-xs-6">
|
||||||
|
|||||||
@@ -13,6 +13,12 @@
|
|||||||
.JobDetail-instructions{
|
.JobDetail-instructions{
|
||||||
color: @default-interface-txt;
|
color: @default-interface-txt;
|
||||||
margin: 10px 0 10px 0;
|
margin: 10px 0 10px 0;
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
background-color: @default-list-header-bg;
|
||||||
|
color: @default-interface-txt;
|
||||||
|
padding: 5px 7px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.JobDetail{
|
.JobDetail{
|
||||||
.OnePlusOne-container(100%, @breakpoint-md);
|
.OnePlusOne-container(100%, @breakpoint-md);
|
||||||
@@ -151,6 +157,10 @@
|
|||||||
background-color: @default-link;
|
background-color: @default-link;
|
||||||
border: 1px solid @default-link;
|
border: 1px solid @default-link;
|
||||||
color: @default-bg;
|
color: @default-bg;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: @default-link-hov;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.JobDetail .nvd3.nv-noData{
|
.JobDetail .nvd3.nv-noData{
|
||||||
color: @default-interface-txt;
|
color: @default-interface-txt;
|
||||||
@@ -198,6 +208,12 @@
|
|||||||
margin-left: -5px;
|
margin-left: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.JobDetails-table--noResults {
|
||||||
|
tr > td {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.JobDetail-statusIcon--results{
|
.JobDetail-statusIcon--results{
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
@@ -215,3 +231,7 @@
|
|||||||
width:0px;
|
width:0px;
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.JobDetail-leftSide.JobDetail-stdoutActionButton--active {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -667,21 +667,9 @@ export default
|
|||||||
|
|
||||||
scope.lessStatus = false; // close the view more status option
|
scope.lessStatus = false; // close the view more status option
|
||||||
|
|
||||||
// Detail table height adjusting. First, put page height back to 'normal'.
|
|
||||||
$('#plays-table-detail').height(80);
|
|
||||||
//$('#plays-table-detail').mCustomScrollbar("update");
|
|
||||||
// $('#tasks-table-detail').height(120);
|
|
||||||
//$('#tasks-table-detail').mCustomScrollbar("update");
|
|
||||||
$('#hosts-table-detail').height(150);
|
|
||||||
//$('#hosts-table-detail').mCustomScrollbar("update");
|
|
||||||
height = $(window).height() - $('#main-menu-container .navbar').outerHeight() -
|
height = $(window).height() - $('#main-menu-container .navbar').outerHeight() -
|
||||||
$('#job-detail-container').outerHeight() - 20;
|
$('#job-detail-container').outerHeight() - 20;
|
||||||
if (height > 15) {
|
|
||||||
// there's a bunch of white space at the bottom, let's use it
|
|
||||||
$('#plays-table-detail').height(80 + (height * 0.10));
|
|
||||||
$('#tasks-table-detail').height(120 + (height * 0.20));
|
|
||||||
$('#hosts-table-detail').height(150 + (height * 0.10));
|
|
||||||
}
|
|
||||||
scope.$emit('RefreshCompleted');
|
scope.$emit('RefreshCompleted');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="plays-table-header" class="table-header">
|
<div id="plays-table-header" class="table-header" ng-show="plays.length !== 0">
|
||||||
<table class="table table-condensed">
|
<table class="table table-condensed">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="plays-table-detail" class="table-detail" lr-infinite-scroll="playsScrollDown"
|
<div id="plays-table-detail" class="table-detail" lr-infinite-scroll="playsScrollDown"
|
||||||
scroll-threshold="10" time-threshold="500">
|
scroll-threshold="10" time-threshold="500">
|
||||||
<table class="table">
|
<table class="table" ng-class="{'JobDetails-table--noResults': plays.length === 0}">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="List-tableRow cursor-pointer" ng-repeat="play in plays" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-class="play.playActiveClass" ng-click="selectPlay(play.id, $event)">
|
<tr class="List-tableRow cursor-pointer" ng-repeat="play in plays" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-class="play.playActiveClass" ng-click="selectPlay(play.id, $event)">
|
||||||
<td class="List-tableCell col-lg-7 col-md-6 col-sm-6 col-xs-4 status-column" aw-tool-tip="{{ play.status_tip }}" data-tip-watch="play.status_tip" data-placement="top"><i class="JobDetail-statusIcon fa icon-job-{{ play.status }}"></i>{{ play.name }}</td>
|
<td class="List-tableCell col-lg-7 col-md-6 col-sm-6 col-xs-4 status-column" aw-tool-tip="{{ play.status_tip }}" data-tip-watch="play.status_tip" data-placement="top"><i class="JobDetail-statusIcon fa icon-job-{{ play.status }}"></i>{{ play.name }}</td>
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<table id="tasks-table-header" class="table table-condensed">
|
<table id="tasks-table-header" class="table table-condensed" ng-show="taskList.length !== 0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="List-tableHeader col-lg-3 col-md-3 col-sm-6 col-xs-4">Tasks</th>
|
<th class="List-tableHeader col-lg-3 col-md-3 col-sm-6 col-xs-4">Tasks</th>
|
||||||
@@ -261,7 +261,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="tasks-table-detail" class="table-detail" lr-infinite-scroll="tasksScrollDown"
|
<div id="tasks-table-detail" class="table-detail" lr-infinite-scroll="tasksScrollDown"
|
||||||
scroll-threshold="10" time-threshold="500">
|
scroll-threshold="10" time-threshold="500">
|
||||||
<table class="table">
|
<table class="table" ng-class="{'JobDetails-table--noResults': taskList.length === 0}">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="task in taskList = (tasks) track by $index" ng-class="task.taskActiveClass" ng-click="selectTask(task.id)">
|
<tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="task in taskList = (tasks) track by $index" ng-class="task.taskActiveClass" ng-click="selectTask(task.id)">
|
||||||
<td class="List-tableCell col-lg-3 col-md-3 col-sm-6 col-xs-4 status-column" aw-tool-tip="{{ task.status_tip }}"
|
<td class="List-tableCell col-lg-3 col-md-3 col-sm-6 col-xs-4 status-column" aw-tool-tip="{{ task.status_tip }}"
|
||||||
@@ -331,7 +331,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-header" id="hosts-table-header">
|
<div class="table-header" id="hosts-table-header">
|
||||||
<table class="table table-condensed">
|
<table class="table table-condensed" ng-show="results.length !== 0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="List-tableHeader col-lg-4 col-md-3 col-sm-3 col-xs-3">Hosts</th>
|
<th class="List-tableHeader col-lg-4 col-md-3 col-sm-3 col-xs-3">Hosts</th>
|
||||||
@@ -343,7 +343,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hosts-table-detail" class="table-detail" lr-infinite-scroll="hostResultsScrollDown" scroll-threshold="10" time-threshold="500">
|
<div id="hosts-table-detail" class="table-detail" lr-infinite-scroll="hostResultsScrollDown" scroll-threshold="10" time-threshold="500">
|
||||||
<table class="table">
|
<table class="table" ng-class="{'JobDetails-table--noResults': results.length === 0}">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="result in results = (hostResults) track by $index">
|
<tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="result in results = (hostResults) track by $index">
|
||||||
<td class="List-tableCell col-lg-4 col-md-3 col-sm-3 col-xs-3 status-column">
|
<td class="List-tableCell col-lg-4 col-md-3 col-sm-3 col-xs-3 status-column">
|
||||||
@@ -404,7 +404,7 @@
|
|||||||
<button class="StandardOut-actionButton" aw-tool-tip="Toggle Output" data-placement="top" ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}" ng-click="toggleStdoutFullscreen()">
|
<button class="StandardOut-actionButton" aw-tool-tip="Toggle Output" data-placement="top" ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}" ng-click="toggleStdoutFullscreen()">
|
||||||
<i class="fa fa-arrows-alt"></i>
|
<i class="fa fa-arrows-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
<a ng-show="job_status.status === ('failed' || 'successful')" href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}">
|
<a ng-show="job_status.status === 'failed' || job_status.status === 'successful' || job_status.status === 'canceled'" href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}">
|
||||||
<button class="StandardOut-actionButton" aw-tool-tip="Download Output" data-placement="top">
|
<button class="StandardOut-actionButton" aw-tool-tip="Download Output" data-placement="top">
|
||||||
<i class="fa fa-download"></i>
|
<i class="fa fa-download"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
jQuery.extend(true, CloudCredentialList, CredentialList);
|
jQuery.extend(true, CloudCredentialList, CredentialList);
|
||||||
CloudCredentialList.name = 'cloudcredentials';
|
CloudCredentialList.name = 'cloudcredentials';
|
||||||
CloudCredentialList.iterator = 'cloudcredential';
|
CloudCredentialList.iterator = 'cloudcredential';
|
||||||
|
CloudCredentialList.basePath = '/api/v1/credentials?cloud=true';
|
||||||
|
|
||||||
SurveyControllerInit({
|
SurveyControllerInit({
|
||||||
scope: $scope,
|
scope: $scope,
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export default
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.error(function (ret,status_code) {
|
.error(function (ret,status_code) {
|
||||||
if (status_code == 403) {
|
if (status_code === 403) {
|
||||||
/* user doesn't have access to see the project, no big deal. */
|
/* user doesn't have access to see the project, no big deal. */
|
||||||
} else {
|
} else {
|
||||||
Alert('Missing Playbooks', 'Unable to retrieve the list of playbooks for this project. Choose a different ' +
|
Alert('Missing Playbooks', 'Unable to retrieve the list of playbooks for this project. Choose a different ' +
|
||||||
@@ -198,7 +198,7 @@ export default
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.error(function (data, status) {
|
.error(function (data, status) {
|
||||||
if (status == 403) {
|
if (status === 403) {
|
||||||
/* User doesn't have read access to the project, no problem. */
|
/* User doesn't have read access to the project, no problem. */
|
||||||
} else {
|
} else {
|
||||||
ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to get project ' + $scope.project +
|
ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to get project ' + $scope.project +
|
||||||
@@ -288,6 +288,7 @@ export default
|
|||||||
jQuery.extend(true, CloudCredentialList, CredentialList);
|
jQuery.extend(true, CloudCredentialList, CredentialList);
|
||||||
CloudCredentialList.name = 'cloudcredentials';
|
CloudCredentialList.name = 'cloudcredentials';
|
||||||
CloudCredentialList.iterator = 'cloudcredential';
|
CloudCredentialList.iterator = 'cloudcredential';
|
||||||
|
CloudCredentialList.basePath = '/api/v1/credentials?cloud=true';
|
||||||
LookUpInit({
|
LookUpInit({
|
||||||
url: GetBasePath('credentials') + '?cloud=true',
|
url: GetBasePath('credentials') + '?cloud=true',
|
||||||
scope: $scope,
|
scope: $scope,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default
|
|||||||
index: false,
|
index: false,
|
||||||
hover: true,
|
hover: true,
|
||||||
well: false,
|
well: false,
|
||||||
|
emptyListText: 'No completed jobs',
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default
|
|||||||
index: true,
|
index: true,
|
||||||
hover: true,
|
hover: true,
|
||||||
well: false,
|
well: false,
|
||||||
|
emptyListText: 'No schedules exist',
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@@ -157,11 +157,11 @@
|
|||||||
ng-href="/#/portal"
|
ng-href="/#/portal"
|
||||||
ng-if="!licenseMissing"
|
ng-if="!licenseMissing"
|
||||||
ng-class="{'is-currentRoute' : isCurrentState('portalMode'), 'is-loggedOut' : !$root.current_user.username}"
|
ng-class="{'is-currentRoute' : isCurrentState('portalMode'), 'is-loggedOut' : !$root.current_user.username}"
|
||||||
aw-tool-tip="Portal Mode"
|
aw-tool-tip="My View"
|
||||||
data-placement="bottom"
|
data-placement="bottom"
|
||||||
data-trigger="hover"
|
data-trigger="hover"
|
||||||
data-container="body">
|
data-container="body">
|
||||||
<i class="MainMenu-itemImage MainMenu-itemImage--settings fa fa-columns"
|
<i class="MainMenu-itemImage MainMenu-itemImage--settings fa fa-tasks"
|
||||||
alt="Portal Mode">
|
alt="Portal Mode">
|
||||||
</i>
|
</i>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<div ui-view></div>
|
||||||
<div class="tab-pane Panel" id="management_jobs">
|
<div class="tab-pane Panel" id="management_jobs">
|
||||||
<div class="List-title">
|
<div class="List-title">
|
||||||
<div class="List-titleText">
|
<div class="List-titleText">
|
||||||
@@ -7,7 +8,6 @@
|
|||||||
{{ mgmtCards.length }}
|
{{ mgmtCards.length }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div ui-view></div>
|
|
||||||
<div class="MgmtCards">
|
<div class="MgmtCards">
|
||||||
<div class="MgmtCards-card"
|
<div class="MgmtCards-card"
|
||||||
ng-repeat="card in mgmtCards track by card.id">
|
ng-repeat="card in mgmtCards track by card.id">
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
.MgmtCards {
|
.MgmtCards {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-flow: row wrap;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MgmtCards-card {
|
.MgmtCards-card {
|
||||||
@@ -11,15 +12,14 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 1px solid @default-border;
|
border: 1px solid @default-border;
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
width: 32%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MgmtCards-card--selected {
|
.MgmtCards-card--selected {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
border-left: 5px solid #337AB7;
|
border-left: 5px solid @default-link;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MgmtCards-card--promptElements{
|
.MgmtCards-card--promptElements{
|
||||||
@@ -44,7 +44,6 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: @default-interface-txt;
|
color: @default-interface-txt;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
@@ -86,46 +85,19 @@
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1179px) {
|
@media (max-width: 840px) {
|
||||||
.MgmtCards-card {
|
|
||||||
width: ~"calc(25% - 15px)";
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MgmtCards-card:nth-child(4n+4) {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 901px) and (max-width: 1178px) {
|
|
||||||
.MgmtCards-card {
|
|
||||||
width: ~"calc(33% - 11px)";
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MgmtCards-card:nth-child(3n+3) {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 616px) and (max-width: 900px) {
|
|
||||||
.MgmtCards-card {
|
|
||||||
width: ~"calc(50% - 10px)";
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MgmtCards-card:nth-child(2n+2) {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 615px) {
|
|
||||||
.MgmtCards-card {
|
.MgmtCards-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 840px) and (max-width: 1240px) {
|
||||||
|
.MgmtCards-card {
|
||||||
|
width: ~"calc(50% - 10px)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#prompt-for-days-facts, #prompt-for-days {
|
#prompt-for-days-facts, #prompt-for-days {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
font-family: "Open Sans";
|
font-family: "Open Sans";
|
||||||
|
|||||||
@@ -141,7 +141,23 @@ export default function() {
|
|||||||
reqExpression: "channel_required",
|
reqExpression: "channel_required",
|
||||||
init: "false"
|
init: "false"
|
||||||
},
|
},
|
||||||
ngShow: "notification_type.value == 'slack' || notification_type.value == 'hipchat'",
|
ngShow: "notification_type.value == 'slack'",
|
||||||
|
subForm: 'typeSubForm'
|
||||||
|
},
|
||||||
|
rooms: {
|
||||||
|
label: 'Destination Channels',
|
||||||
|
type: 'textarea',
|
||||||
|
rows: 3,
|
||||||
|
awPopOver: '<p>Type an option on each line. The pound symbol (#) is not required.</p>'+
|
||||||
|
'<p>For example:<br>engineering<br>\n #support<br>\n',
|
||||||
|
dataTitle: 'Destination Channels',
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataContainer: "body",
|
||||||
|
awRequiredWhen: {
|
||||||
|
reqExpression: "room_required",
|
||||||
|
init: "false"
|
||||||
|
},
|
||||||
|
ngShow: "notification_type.value == 'hipchat'",
|
||||||
subForm: 'typeSubForm'
|
subForm: 'typeSubForm'
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
@@ -243,8 +259,9 @@ export default function() {
|
|||||||
subForm: 'typeSubForm'
|
subForm: 'typeSubForm'
|
||||||
},
|
},
|
||||||
api_url: {
|
api_url: {
|
||||||
label: 'API URL (e.g: https://mycompany.hiptchat.com)',
|
label: 'API URL',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
placeholder: 'https://mycompany.hipchat.com',
|
||||||
awRequiredWhen: {
|
awRequiredWhen: {
|
||||||
reqExpression: "hipchat_required",
|
reqExpression: "hipchat_required",
|
||||||
init: "false"
|
init: "false"
|
||||||
@@ -264,11 +281,7 @@ export default function() {
|
|||||||
},
|
},
|
||||||
notify: {
|
notify: {
|
||||||
label: 'Notify Channel',
|
label: 'Notify Channel',
|
||||||
type: 'text',
|
type: 'checkbox',
|
||||||
awRequiredWhen: {
|
|
||||||
reqExpression: "hipchat_required",
|
|
||||||
init: "false"
|
|
||||||
},
|
|
||||||
ngShow: "notification_type.value == 'hipchat' ",
|
ngShow: "notification_type.value == 'hipchat' ",
|
||||||
subForm: 'typeSubForm'
|
subForm: 'typeSubForm'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default function(){
|
|||||||
iterator: 'notification_template',
|
iterator: 'notification_template',
|
||||||
index: false,
|
index: false,
|
||||||
hover: false,
|
hover: false,
|
||||||
|
emptyListText: 'No notifications exist',
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default function(){
|
|||||||
iterator: 'notification',
|
iterator: 'notification',
|
||||||
index: false,
|
index: false,
|
||||||
hover: false,
|
hover: false,
|
||||||
|
emptyListText: 'No Notifications exist',
|
||||||
basePath: 'notifications',
|
basePath: 'notifications',
|
||||||
fields: {
|
fields: {
|
||||||
name: {
|
name: {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function () {
|
|||||||
case 'hipchat':
|
case 'hipchat':
|
||||||
obj.tokenLabel = ' Token';
|
obj.tokenLabel = ' Token';
|
||||||
obj.hipchat_required = true;
|
obj.hipchat_required = true;
|
||||||
obj.channel_required = true;
|
obj.room_required = true;
|
||||||
obj.token_required = true;
|
obj.token_required = true;
|
||||||
break;
|
break;
|
||||||
case 'twilio':
|
case 'twilio':
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<div id="scheduled_jobs_link" class="Form-tab"
|
<div id="scheduled_jobs_link" class="Form-tab"
|
||||||
ng-class="{'is-selected': schedulesSelected }"
|
ng-class="{'is-selected': schedulesSelected }"
|
||||||
ng-click="toggleTab('scheduled')">
|
ng-click="toggleTab('scheduled')">
|
||||||
Schedule
|
Schedules
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="jobs-tab-content" class="Form-tabSection"
|
<div id="jobs-tab-content" class="Form-tabSection"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default {
|
|||||||
name: 'portalMode',
|
name: 'portalMode',
|
||||||
url: '/portal',
|
url: '/portal',
|
||||||
ncyBreadcrumb: {
|
ncyBreadcrumb: {
|
||||||
label: "PORTAL MODE"
|
label: "MY VIEW"
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
features: ['FeaturesService', function(FeaturesService) {
|
features: ['FeaturesService', function(FeaturesService) {
|
||||||
@@ -30,4 +30,4 @@ export default {
|
|||||||
controller: PortalModeJobsController
|
controller: PortalModeJobsController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,19 +37,28 @@ export default
|
|||||||
name: 'projectSchedules',
|
name: 'projectSchedules',
|
||||||
route: '/projects/:id/schedules',
|
route: '/projects/:id/schedules',
|
||||||
templateUrl: templateUrl("scheduler/scheduler"),
|
templateUrl: templateUrl("scheduler/scheduler"),
|
||||||
controller: 'schedulerController'
|
controller: 'schedulerController',
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
label: 'PROJECT SCHEDULES'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$stateExtender.addState({
|
$stateExtender.addState({
|
||||||
name: 'projectSchedules.add',
|
name: 'projectSchedules.add',
|
||||||
route: '/add',
|
route: '/add',
|
||||||
templateUrl: templateUrl("scheduler/schedulerForm"),
|
templateUrl: templateUrl("scheduler/schedulerForm"),
|
||||||
controller: 'schedulerAddController'
|
controller: 'schedulerAddController',
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
label: 'PROJECT SCHEDULES ADD'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$stateExtender.addState({
|
$stateExtender.addState({
|
||||||
name: 'projectSchedules.edit',
|
name: 'projectSchedules.edit',
|
||||||
route: '/:schedule_id',
|
route: '/:schedule_id',
|
||||||
templateUrl: templateUrl("scheduler/schedulerForm"),
|
templateUrl: templateUrl("scheduler/schedulerForm"),
|
||||||
controller: 'schedulerEditController'
|
controller: 'schedulerEditController',
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
label: 'PROJECT SCHEDULES EDIT'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$stateExtender.addState({
|
$stateExtender.addState({
|
||||||
name: 'inventoryManage.schedules',
|
name: 'inventoryManage.schedules',
|
||||||
|
|||||||
@@ -509,7 +509,7 @@
|
|||||||
Please provide a valid date.
|
Please provide a valid date.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group SchedulerForm-formGroup"
|
<div class="form-group SchedulerForm-formGroup"
|
||||||
ng-if="schedulerEnd && schedulerEnd.value == 'on'">
|
ng-if="schedulerEnd && schedulerEnd.value == 'on'">
|
||||||
<label class="Form-inputLabel">
|
<label class="Form-inputLabel">
|
||||||
<span class="red-text">*</span>
|
<span class="red-text">*</span>
|
||||||
@@ -583,7 +583,7 @@
|
|||||||
<div class="SchedulerFormDetail-container"
|
<div class="SchedulerFormDetail-container"
|
||||||
ng-show="schedulerIsValid">
|
ng-show="schedulerIsValid">
|
||||||
<label class="SchedulerFormDetail-label">
|
<label class="SchedulerFormDetail-label">
|
||||||
Description
|
Schedule Description
|
||||||
</label>
|
</label>
|
||||||
<div class="SchedulerFormDetail-nlp">
|
<div class="SchedulerFormDetail-nlp">
|
||||||
{{ rrule_nlp_description }}
|
{{ rrule_nlp_description }}
|
||||||
@@ -651,7 +651,7 @@
|
|||||||
<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>"
|
<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>"
|
||||||
data-placement="right" data-container="body" over-title="Extra Variables" class="help-link" data-original-title="" title="" tabindex="-1">
|
data-placement="right" data-container="body" over-title="Extra Variables" class="help-link" data-original-title="" title="" tabindex="-1">
|
||||||
<i class="fa fa-question-circle"></i>
|
<i class="fa fa-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="parse-selection">
|
<div class="parse-selection">
|
||||||
<input type="radio" ng-model="parseType" ng-change="parseTypeChange()" value="yaml"><span class="parse-label">YAML</span>
|
<input type="radio" ng-model="parseType" ng-change="parseTypeChange()" value="yaml"><span class="parse-label">YAML</span>
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
overflow-y: scroll;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default {
|
|||||||
name: 'setup',
|
name: 'setup',
|
||||||
route: '/setup',
|
route: '/setup',
|
||||||
ncyBreadcrumb: {
|
ncyBreadcrumb: {
|
||||||
label: "SETUP"
|
label: "SETTINGS"
|
||||||
},
|
},
|
||||||
templateUrl: templateUrl('setup-menu/setup-menu')
|
templateUrl: templateUrl('setup-menu/setup-menu')
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
|
|||||||
callback = params.callback,
|
callback = params.callback,
|
||||||
beforeDestroy = params.beforeDestroy,
|
beforeDestroy = params.beforeDestroy,
|
||||||
closeOnEscape = (params.closeOnEscape === undefined) ? false : params.closeOnEscape,
|
closeOnEscape = (params.closeOnEscape === undefined) ? false : params.closeOnEscape,
|
||||||
resizable = (params.resizable === undefined) ? true : params.resizable,
|
resizable = (params.resizable === undefined) ? false : params.resizable,
|
||||||
draggable = (params.draggable === undefined) ? true : params.draggable,
|
draggable = (params.draggable === undefined) ? true : params.draggable,
|
||||||
dialogClass = params.dialogClass,
|
dialogClass = params.dialogClass,
|
||||||
forms = _.chain([params.form]).flatten().compact().value(),
|
forms = _.chain([params.form]).flatten().compact().value(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// default base colors
|
// default base colors
|
||||||
@default-interface-txt: #848992;
|
@default-interface-txt: #707070;
|
||||||
@default-data-txt: #161B1F;
|
@default-data-txt: #161B1F;
|
||||||
@default-icon: #B7B7B7;
|
@default-icon: #848992;
|
||||||
@default-icon-hov: #D7D7D7; // also selected button
|
@default-icon-hov: #D7D7D7; // also selected button
|
||||||
@default-border: #E8E8E8;
|
@default-border: #E8E8E8;
|
||||||
@d7grey: #D7D7D7; // used for random things, like the close button on top-right corner of panes
|
@d7grey: #D7D7D7; // used for random things, like the close button on top-right corner of panes
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
@default-stdout-txt: #707070;
|
@default-stdout-txt: #707070;
|
||||||
@default-dark: #000000;
|
@default-dark: #000000;
|
||||||
|
|
||||||
|
|
||||||
@default-warning: #F0AD4E;
|
@default-warning: #F0AD4E;
|
||||||
@default-warning-hov: #EC971F;
|
@default-warning-hov: #EC971F;
|
||||||
@default-unreachable: #FF0000;
|
@default-unreachable: #FF0000;
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
|
|
||||||
// buttons
|
// buttons
|
||||||
@btn-bg: @default-bg;
|
@btn-bg: @default-bg;
|
||||||
@btn-bord: @default-border;
|
@btn-bord: @d7grey;
|
||||||
@btn-txt: @default-interface-txt;
|
@btn-txt: @default-interface-txt;
|
||||||
@btn-bg-hov: @default-tertiary-bg;
|
@btn-bg-hov: @default-tertiary-bg;
|
||||||
@btn-bg-sel: @default-icon-hov;
|
@btn-bg-sel: @default-icon-hov;
|
||||||
|
|||||||
@@ -1838,7 +1838,11 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Show the "no items" box when loading is done and the user isn't actively searching and there are no results
|
// Show the "no items" box when loading is done and the user isn't actively searching and there are no results
|
||||||
html += "<div class=\"List-noItems\" ng-show=\"" + collection.iterator + "Loading == false && " + collection.iterator + "_active_search == false && " + collection.iterator + "_total_rows < 1\">PLEASE ADD ITEMS TO THIS LIST</div>";
|
// Allow for the suppression of the empty list text to avoid duplication between form generator and list generator
|
||||||
|
if(!collection.suppressEmptyText || collection.suppressEmptyText === false) {
|
||||||
|
var emptyListText = (collection.emptyListText) ? collection.emptyListText : "PLEASE ADD ITEMS TO THIS LIST";
|
||||||
|
html += "<div class=\"List-noItems\" ng-show=\"" + collection.iterator + "Loading == false && " + collection.iterator + "_active_search == false && " + collection.iterator + "_total_rows < 1\">" + emptyListText + "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class=\"List-noItems\" ng-show=\"is_superuser\">
|
<div class=\"List-noItems\" ng-show=\"is_superuser\">
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
|
|||||||
});
|
});
|
||||||
|
|
||||||
function loadStdout() {
|
function loadStdout() {
|
||||||
Rest.setUrl($scope.stdoutEndpoint + '?format=json&start_line=-' + page_size);
|
Rest.setUrl($scope.stdoutEndpoint + '?format=json&start_line=0&end_line=' + page_size);
|
||||||
Rest.get()
|
Rest.get()
|
||||||
.success(function(data) {
|
.success(function(data) {
|
||||||
Wait('stop');
|
Wait('stop');
|
||||||
@@ -145,38 +145,17 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.stdOutScrollToTop = function() {
|
// lrInfiniteScroll handler
|
||||||
// scroll up or back in time toward the beginning of the file
|
// grabs the next stdout section
|
||||||
var start, end, url;
|
$scope.stdOutGetNextSection = function(){
|
||||||
if (loaded_sections.length > 0 && loaded_sections[0].start > 0) {
|
if (current_range.absolute_end > current_range.end){
|
||||||
start = (loaded_sections[0].start - page_size > 0) ? loaded_sections[0].start - page_size : 0;
|
var url = $scope.stdoutEndpoint + '?format=json&start_line=' + current_range.end +
|
||||||
end = loaded_sections[0].start - 1;
|
'&end_line=' + (current_range.end + page_size);
|
||||||
}
|
|
||||||
else if (loaded_sections.length === 0) {
|
|
||||||
start = 0;
|
|
||||||
end = page_size;
|
|
||||||
}
|
|
||||||
if (start !== undefined && end !== undefined) {
|
|
||||||
$('#stdoutMoreRowsTop').fadeIn();
|
|
||||||
url = stdout_url + '?format=json&start_line=' + start + '&end_line=' + end;
|
|
||||||
Rest.setUrl(url);
|
Rest.setUrl(url);
|
||||||
Rest.get()
|
Rest.get()
|
||||||
.success( function(data) {
|
.success(function(data){
|
||||||
//var currentPos = $('#pre-container').scrollTop();
|
$('#pre-container-content').append(data.content);
|
||||||
var newSH, oldSH = $('#pre-container').prop('scrollHeight'),
|
|
||||||
st = $('#pre-container').scrollTop();
|
|
||||||
|
|
||||||
$('#pre-container-content').prepend(data.content);
|
|
||||||
|
|
||||||
newSH = $('#pre-container').prop('scrollHeight');
|
|
||||||
$('#pre-container').scrollTop(newSH - oldSH + st);
|
|
||||||
|
|
||||||
loaded_sections.unshift({
|
|
||||||
start: (data.range.start < 0) ? 0 : data.range.start,
|
|
||||||
end: data.range.end
|
|
||||||
});
|
|
||||||
current_range = data.range;
|
current_range = data.range;
|
||||||
$('#stdoutMoreRowsTop').fadeOut(400);
|
|
||||||
})
|
})
|
||||||
.error(function(data, status) {
|
.error(function(data, status) {
|
||||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<div class="StandardOut-consoleOutput">
|
<div class="StandardOut-consoleOutput" lr-infinite-scroll="stdOutGetNextSection" scroll-threshold="300" time-threshold="500">
|
||||||
<div id="pre-container" class="body_background body_foreground pre mono-space StandardOut-preContainer"
|
<div id="pre-container" class="body_background body_foreground pre mono-space StandardOut-preContainer">
|
||||||
lr-infinite-scroll="stdOutScrollToTop" scroll-threshold="300" data-direction="up" time-threshold="500">
|
|
||||||
<div id="pre-container-content" class="StandardOut-preContent"></div>
|
<div id="pre-container-content" class="StandardOut-preContent"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scroll-spinner" id="stdoutMoreRowsBottom">
|
<div class="scroll-spinner" id="stdoutMoreRowsBottom">
|
||||||
|
|||||||
829
npm-shrinkwrap.json
generated
829
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,8 @@ ENV LC_ALL en_US.UTF-8
|
|||||||
RUN apt-get update && apt-get install -y software-properties-common python-software-properties curl
|
RUN apt-get update && apt-get install -y software-properties-common python-software-properties curl
|
||||||
RUN add-apt-repository -y ppa:chris-lea/redis-server; add-apt-repository -y ppa:chris-lea/zeromq; add-apt-repository -y ppa:chris-lea/node.js; add-apt-repository -y ppa:ansible/ansible; add-apt-repository -y ppa:jal233/proot;
|
RUN add-apt-repository -y ppa:chris-lea/redis-server; add-apt-repository -y ppa:chris-lea/zeromq; add-apt-repository -y ppa:chris-lea/node.js; add-apt-repository -y ppa:ansible/ansible; add-apt-repository -y ppa:jal233/proot;
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_0.12 | bash -
|
RUN curl -sL https://deb.nodesource.com/setup_0.12 | bash -
|
||||||
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 && apt-key adv --fetch-keys http://www.postgresql.org/media/keys/ACCC4CF8.asc
|
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
|
||||||
|
RUN curl -sL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||||
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main" | tee /etc/apt/sources.list.d/postgres-9.4.list
|
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main" | tee /etc/apt/sources.list.d/postgres-9.4.list
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install -y openssh-server ansible mg vim tmux git mercurial subversion python-dev python-psycopg2 make postgresql-client libpq-dev nodejs python-psutil libxml2-dev libxslt-dev lib32z1-dev libsasl2-dev libldap2-dev libffi-dev libzmq-dev proot python-pip libxmlsec1-dev swig redis-server && rm -rf /var/lib/apt/lists/*
|
RUN apt-get install -y openssh-server ansible mg vim tmux git mercurial subversion python-dev python-psycopg2 make postgresql-client libpq-dev nodejs python-psutil libxml2-dev libxslt-dev lib32z1-dev libsasl2-dev libldap2-dev libffi-dev libzmq-dev proot python-pip libxmlsec1-dev swig redis-server && rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
Reference in New Issue
Block a user