mirror of
https://github.com/ansible/awx.git
synced 2026-01-22 15:08:03 -03:30
Merge branch 'release_3.0.0' of github.com:ansible/ansible-tower into JobDetailsAudit
This commit is contained in:
commit
cc507cee1f
@ -219,7 +219,7 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
else:
|
||||
q = Q(**{k:v})
|
||||
queryset = queryset.filter(q)
|
||||
queryset = queryset.filter(*args)
|
||||
queryset = queryset.filter(*args).distinct()
|
||||
return queryset
|
||||
except (FieldError, FieldDoesNotExist, ValueError), e:
|
||||
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_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,)),
|
||||
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,)),
|
||||
labels = reverse('api:job_template_label_list', args=(obj.pk,)),
|
||||
roles = reverse('api:job_template_roles_list', args=(obj.pk,)),
|
||||
|
||||
@ -2212,6 +2212,13 @@ class JobTemplateList(ListCreateAPIView):
|
||||
serializer_class = JobTemplateSerializer
|
||||
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):
|
||||
|
||||
model = JobTemplate
|
||||
|
||||
@ -773,7 +773,9 @@ class JobTemplateAccess(BaseAccess):
|
||||
inventory_pk = get_pk_from_dict(data, 'inventory')
|
||||
inventory = Inventory.objects.filter(id=inventory_pk)
|
||||
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')
|
||||
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
|
||||
# be able to proceed without additional checks.
|
||||
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):
|
||||
# Check license.
|
||||
@ -817,13 +817,62 @@ class JobTemplateAccess(BaseAccess):
|
||||
if self.user not in obj.admin_role:
|
||||
return False
|
||||
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'):
|
||||
required_obj = getattr(obj, required_field, None)
|
||||
if required_field not in data_for_change and required_obj is not None:
|
||||
data_for_change[required_field] = required_obj.pk
|
||||
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):
|
||||
return self.user in obj.admin_role
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
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 _migration_utils as migration_utils
|
||||
from django.db import migrations
|
||||
@ -15,4 +16,5 @@ class Migration(migrations.Migration):
|
||||
operations = [
|
||||
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
||||
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')
|
||||
Permission = apps.get_model('main', 'Permission')
|
||||
|
||||
def role_from_permission():
|
||||
def role_from_permission(perm):
|
||||
if perm.permission_type == 'admin':
|
||||
return inventory.admin_role
|
||||
elif perm.permission_type == 'read':
|
||||
@ -233,7 +233,7 @@ def migrate_inventory(apps, schema_editor):
|
||||
role = None
|
||||
execrole = None
|
||||
|
||||
role = role_from_permission()
|
||||
role = role_from_permission(perm)
|
||||
if role is None:
|
||||
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)))
|
||||
|
||||
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)))
|
||||
|
||||
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):
|
||||
# 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:
|
||||
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)))
|
||||
|
||||
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)))
|
||||
|
||||
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
|
||||
def migrate_job_templates(apps, schema_editor):
|
||||
@ -403,7 +409,7 @@ def migrate_job_templates(apps, schema_editor):
|
||||
|
||||
team_create_permissions = set(
|
||||
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)
|
||||
)
|
||||
team_run_permissions = set(
|
||||
@ -413,12 +419,12 @@ def migrate_job_templates(apps, schema_editor):
|
||||
)
|
||||
user_create_permissions = set(
|
||||
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)
|
||||
)
|
||||
user_run_permissions = set(
|
||||
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)
|
||||
)
|
||||
|
||||
@ -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)))
|
||||
|
||||
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
|
||||
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
|
||||
Credential.objects.filter(Q(deprecated_user=user) | Q(deprecated_team__deprecated_users=user), jobtemplates=jt).exists()):
|
||||
(not cred or cred.deprecated_user == user or
|
||||
(cred.deprecated_team and cred.deprecated_team.deprecated_users.filter(pk=user.id).exists())):
|
||||
jt.admin_role.members.add(user)
|
||||
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():
|
||||
cred = jt.credential or jt.cloud_credential
|
||||
|
||||
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 \
|
||||
((not jt.credential and not jt.cloud_credential) or
|
||||
Credential.objects.filter(Q(deprecated_user=user) | Q(deprecated_team__deprecated_users=user), jobtemplates=jt).exists()):
|
||||
(not cred or cred.deprecated_user == user or
|
||||
(cred.deprecated_team and cred.deprecated_team.deprecated_users.filter(pk=user.id).exists())):
|
||||
jt.execute_role.members.add(user)
|
||||
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()
|
||||
roots = Role.objects \
|
||||
.all() \
|
||||
.exclude(pk__in=Role.parents.through.objects.all()
|
||||
.values_list('from_role_id', flat=True).distinct()) \
|
||||
.values_list('id', flat=True)
|
||||
stop = time()
|
||||
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
|
||||
object1=camelcase_to_underscore(obj1.__class__.__name__)
|
||||
obj_rel = sender.__module__ + "." + sender.__name__
|
||||
|
||||
for entity_acted in kwargs['pk_set']:
|
||||
obj2 = kwargs['model']
|
||||
obj2_id = entity_acted
|
||||
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.
|
||||
if isinstance(obj1, InventorySource) or isinstance(obj2_actual, InventorySource):
|
||||
continue
|
||||
@ -409,7 +414,7 @@ def activity_stream_associate(sender, instance, **kwargs):
|
||||
# If the m2m is from the User side we need to
|
||||
# set the content_object of the Role for our entry.
|
||||
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.object_relationship_type = obj_rel
|
||||
|
||||
@ -441,7 +441,12 @@ class BaseTask(Task):
|
||||
if settings.ANSIBLE_USE_VENV:
|
||||
env['VIRTUAL_ENV'] = settings.ANSIBLE_VENV_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):
|
||||
env['PROOT_TMP_DIR'] = tower_settings.AWX_PROOT_BASE_PATH
|
||||
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(team__pk=team.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.tests.factories import (
|
||||
create_organization,
|
||||
create_job_template,
|
||||
create_notification_template,
|
||||
)
|
||||
|
||||
'''
|
||||
Disable all django model signals.
|
||||
'''
|
||||
@ -147,18 +153,6 @@ def instance(settings):
|
||||
def organization(instance):
|
||||
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
|
||||
def credential():
|
||||
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,},
|
||||
}
|
||||
|
||||
@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
|
||||
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])
|
||||
if 'format' not in kwargs:
|
||||
kwargs['format'] = 'json'
|
||||
@ -311,12 +290,16 @@ def post():
|
||||
response = view(request, *view_args, **view_kwargs)
|
||||
if middleware:
|
||||
middleware.process_response(request, response)
|
||||
if expect:
|
||||
if response.status_code != expect:
|
||||
print(response.data)
|
||||
assert response.status_code == expect
|
||||
return response
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
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])
|
||||
if 'format' not in kwargs:
|
||||
kwargs['format'] = 'json'
|
||||
@ -328,12 +311,16 @@ def get():
|
||||
response = view(request, *view_args, **view_kwargs)
|
||||
if middleware:
|
||||
middleware.process_response(request, response)
|
||||
if expect:
|
||||
if response.status_code != expect:
|
||||
print(response.data)
|
||||
assert response.status_code == expect
|
||||
return response
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
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])
|
||||
if 'format' not in kwargs:
|
||||
kwargs['format'] = 'json'
|
||||
@ -345,12 +332,16 @@ def put():
|
||||
response = view(request, *view_args, **view_kwargs)
|
||||
if middleware:
|
||||
middleware.process_response(request, response)
|
||||
if expect:
|
||||
if response.status_code != expect:
|
||||
print(response.data)
|
||||
assert response.status_code == expect
|
||||
return response
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
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])
|
||||
if 'format' not in kwargs:
|
||||
kwargs['format'] = 'json'
|
||||
@ -362,12 +353,16 @@ def patch():
|
||||
response = view(request, *view_args, **view_kwargs)
|
||||
if middleware:
|
||||
middleware.process_response(request, response)
|
||||
if expect:
|
||||
if response.status_code != expect:
|
||||
print(response.data)
|
||||
assert response.status_code == expect
|
||||
return response
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
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])
|
||||
if 'format' not in kwargs:
|
||||
kwargs['format'] = 'json'
|
||||
@ -379,12 +374,16 @@ def delete():
|
||||
response = view(request, *view_args, **view_kwargs)
|
||||
if middleware:
|
||||
middleware.process_response(request, response)
|
||||
if expect:
|
||||
if response.status_code != expect:
|
||||
print(response.data)
|
||||
assert response.status_code == expect
|
||||
return response
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
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])
|
||||
if 'format' not in kwargs:
|
||||
kwargs['format'] = 'json'
|
||||
@ -396,12 +395,16 @@ def head():
|
||||
response = view(request, *view_args, **view_kwargs)
|
||||
if middleware:
|
||||
middleware.process_response(request, response)
|
||||
if expect:
|
||||
if response.status_code != expect:
|
||||
print(response.data)
|
||||
assert response.status_code == expect
|
||||
return response
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
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])
|
||||
if 'format' not in kwargs:
|
||||
kwargs['format'] = 'json'
|
||||
@ -413,6 +416,10 @@ def options():
|
||||
response = view(request, *view_args, **view_kwargs)
|
||||
if middleware:
|
||||
middleware.process_response(request, response)
|
||||
if expect:
|
||||
if response.status_code != expect:
|
||||
print(response.data)
|
||||
assert response.status_code == expect
|
||||
return response
|
||||
return rf
|
||||
|
||||
@ -474,3 +481,16 @@ def job_template_labels(organization, job_template):
|
||||
job_template.labels.create(name="label-2", organization=organization)
|
||||
|
||||
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 pytest
|
||||
|
||||
from django.db import transaction
|
||||
from django.core.urlresolvers import reverse
|
||||
from awx.main.models import Project
|
||||
|
||||
@ -9,62 +8,55 @@ from awx.main.models import Project
|
||||
#
|
||||
# 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
|
||||
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'
|
||||
|
||||
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')
|
||||
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
|
||||
assert get(reverse('api:user_projects_list', args=(objects.superusers.admin.pk,)), objects.superusers.admin).data['count'] == 3
|
||||
|
||||
# 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=(bob.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=(objects.users.bob.pk,)), objects.superusers.admin).data['count'] == 2
|
||||
|
||||
# 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
|
||||
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
|
||||
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
|
||||
def test_team_project_list(get, project_factory, team_factory, admin, alice, bob):
|
||||
'List of projects a team has access to, filtered by projects you can also see'
|
||||
team1, team2 = setup_test_team_project_list(project_factory, team_factory, admin, alice, bob)
|
||||
def test_team_project_list(get, team_project_list):
|
||||
objects = team_project_list
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_team_project_list_fail1(get, project_factory, team_factory, admin, alice, bob):
|
||||
# alice should not be able to see team2 projects because she doesn't have access to team2
|
||||
team1, team2 = setup_test_team_project_list(project_factory, team_factory, admin, alice, bob)
|
||||
res = get(reverse('api:team_projects_list', args=(team2.pk,)), alice)
|
||||
def test_team_project_list_fail1(get, team_project_list):
|
||||
objects = team_project_list
|
||||
res = get(reverse('api:team_projects_list', args=(objects.teams.team2.pk,)), objects.users.alice)
|
||||
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", [
|
||||
('rando', 403),
|
||||
('org_member', 403),
|
||||
|
||||
@ -7,8 +7,11 @@ from awx.main.access import (
|
||||
)
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from awx.main.models import Permission
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
from django.apps import apps
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
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
|
||||
assert access.can_read(deploy_jobtemplate)
|
||||
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)
|
||||
|
||||
@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'''
|
||||
a = user('admin', False)
|
||||
org_no_members = organization_factory("no_members")
|
||||
org_members = organization_factory("has_members")
|
||||
no_members = organization_factory("no_members")
|
||||
members = organization_factory("has_members",
|
||||
users=['admin'],
|
||||
labels=['test'])
|
||||
|
||||
label.organization.admin_role.members.add(a)
|
||||
org_members.admin_role.members.add(a)
|
||||
label = members.labels.test
|
||||
admin = members.users.admin
|
||||
members.organization.admin_role.members.add(admin)
|
||||
|
||||
access = LabelAccess(user('admin', False))
|
||||
assert not access.can_change(label, {'organization': org_no_members.id})
|
||||
access = LabelAccess(admin)
|
||||
assert not access.can_change(label, {'organization': no_members.organization.id})
|
||||
assert access.can_read(label)
|
||||
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)
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@ -25,35 +25,44 @@ def test_notification_template_get_queryset_orgadmin(notification_template, user
|
||||
assert access.get_queryset().count() == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notification_template_access_superuser(notification_template, user, notification_template_factory):
|
||||
access = NotificationTemplateAccess(user('admin', True))
|
||||
assert access.can_read(notification_template)
|
||||
assert access.can_change(notification_template, None)
|
||||
assert access.can_delete(notification_template)
|
||||
nf = notification_template_factory("test-orphaned")
|
||||
def test_notification_template_access_superuser(notification_template_factory):
|
||||
nf_objects = notification_template_factory('test-orphaned', organization='test', superusers=['admin'])
|
||||
admin = nf_objects.superusers.admin
|
||||
nf = nf_objects.notification_template
|
||||
|
||||
access = NotificationTemplateAccess(admin)
|
||||
assert access.can_read(nf)
|
||||
assert access.can_change(nf, None)
|
||||
assert access.can_delete(nf)
|
||||
|
||||
nf.organization = None
|
||||
nf.save()
|
||||
|
||||
assert access.can_read(nf)
|
||||
assert access.can_change(nf, None)
|
||||
assert access.can_delete(nf)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notification_template_access_admin(notification_template, user, organization_factory, notification_template_factory):
|
||||
adm = user('admin', False)
|
||||
other_org = organization_factory('other')
|
||||
present_org = organization_factory('present')
|
||||
notification_template.organization.admin_role.members.add(adm)
|
||||
present_org.admin_role.members.add(adm)
|
||||
def test_notification_template_access_admin(organization_factory, notification_template_factory):
|
||||
other_objects = organization_factory('other')
|
||||
present_objects = organization_factory('present',
|
||||
users=['admin'],
|
||||
notification_templates=['test-notification'],
|
||||
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 access.can_read(notification_template)
|
||||
assert access.can_change(notification_template, None)
|
||||
assert access.can_change(notification_template, {'organization': present_org.id})
|
||||
assert access.can_delete(notification_template)
|
||||
|
||||
nf = notification_template_factory("test-orphaned")
|
||||
nf.organization = None
|
||||
nf.save()
|
||||
assert not access.can_read(nf)
|
||||
assert not access.can_change(nf, None)
|
||||
assert not access.can_delete(nf)
|
||||
|
||||
@ -404,164 +404,6 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
||||
del data[k]
|
||||
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)
|
||||
def test_ad_hoc_command_detail(self, ignore):
|
||||
@ -953,98 +795,6 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
||||
self.patch(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):
|
||||
# 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)
|
||||
|
||||
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_password = getattr(settings, 'TEST_RACKSPACE_API_KEY', '')
|
||||
source_regions = getattr(settings, 'TEST_RACKSPACE_REGIONS', '')
|
||||
|
||||
@ -961,7 +961,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
||||
self.assertEqual(jobs_qs.count(), 7)
|
||||
job = jobs_qs[0]
|
||||
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.all()[0], host)
|
||||
|
||||
|
||||
@ -37,13 +37,13 @@ body .navbar {
|
||||
border-color: #E8E8E8;
|
||||
}
|
||||
body .navbar .navbar-brand {
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
body .navbar .navbar-brand:focus,
|
||||
body .navbar .navbar-brand:hover {
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
}
|
||||
body .navbar .navbar-brand img {
|
||||
display: inline-block;
|
||||
@ -60,7 +60,7 @@ body .navbar .navbar-brand > span {
|
||||
body .navbar .navbar-title {
|
||||
float: left;
|
||||
height: 50px;
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
@ -74,19 +74,19 @@ body.show-title .navbar .navbar-title {
|
||||
display: inline-block;
|
||||
}
|
||||
body .navbar .navbar-nav > li > a {
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
body .navbar .navbar-nav > li > a:focus,
|
||||
body .navbar .navbar-nav > li > a:hover {
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
}
|
||||
body .navbar .navbar-nav > li > a > span.glyphicon {
|
||||
font-size: 20px;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
color: #B7B7B7;
|
||||
color: #848992;
|
||||
}
|
||||
|
||||
body .page-header {
|
||||
@ -110,7 +110,7 @@ body .description .hide-description span.glyphicon {
|
||||
font-size: 20px;
|
||||
}
|
||||
body .description .hide-description:hover span.glyphicon {
|
||||
color: #B7B7B7;
|
||||
color: #848992;
|
||||
}
|
||||
body ul.breadcrumb,
|
||||
body .description,
|
||||
@ -167,7 +167,7 @@ body .form-actions button {
|
||||
body .form-horizontal .control-label {
|
||||
text-transform: uppercase;
|
||||
font-weight: normal;
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
}
|
||||
body textarea.form-control {
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
@ -182,22 +182,22 @@ body .description pre {
|
||||
}
|
||||
|
||||
body .tooltip.bottom .tooltip-arrow {
|
||||
border-bottom-color: #848992;
|
||||
border-bottom-color: #707070;
|
||||
}
|
||||
body .tooltip.top .tooltip-arrow {
|
||||
border-top-color: #848992;
|
||||
border-top-color: #707070;
|
||||
}
|
||||
body .tooltip.left .tooltip-arrow {
|
||||
border-left-color: #848992;
|
||||
border-left-color: #707070;
|
||||
}
|
||||
body .tooltip.right .tooltip-arrow {
|
||||
border-right-color: #848992;
|
||||
border-right-color: #707070;
|
||||
}
|
||||
body .tooltip.in {
|
||||
opacity: 1;
|
||||
}
|
||||
body .tooltip-inner {
|
||||
background-color: #848992;
|
||||
background-color: #707070;
|
||||
}
|
||||
|
||||
body .btn {
|
||||
@ -205,7 +205,7 @@ body .btn {
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: #FFFFFF;
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
border: 1px solid #E8E8E8;
|
||||
}
|
||||
.btn-primary:hover,
|
||||
@ -224,7 +224,7 @@ body .btn {
|
||||
.open>.dropdown-toggle.btn-primary:hover,
|
||||
.open>.dropdown-toggle.btn-primary {
|
||||
background-color: #FAFAFA;
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
border: 1px solid #E8E8E8;
|
||||
}
|
||||
|
||||
@ -283,7 +283,7 @@ body #footer {
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
height: 40px;
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
}
|
||||
body #footer .footer-logo {
|
||||
text-align: left;
|
||||
@ -302,7 +302,7 @@ body #footer .footer-copyright {
|
||||
padding-top: 10px;
|
||||
}
|
||||
body #footer .footer-copyright a {
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
@ -329,7 +329,7 @@ body #footer .footer-copyright a {
|
||||
border-color: #E8E8E8;
|
||||
}
|
||||
body .navbar .navbar-toggle .icon-bar {
|
||||
background-color: #B7B7B7;
|
||||
background-color: #848992;
|
||||
}
|
||||
body .navbar .tooltip {
|
||||
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 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.checkbox-inline.stack-inline {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-options {
|
||||
|
||||
@ -178,7 +178,7 @@
|
||||
|
||||
.Form-formGroup--checkbox{
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.Form-subForm {
|
||||
@ -324,6 +324,12 @@
|
||||
|
||||
.select2-dropdown{
|
||||
border:1px solid @field-border;
|
||||
|
||||
}
|
||||
|
||||
.select2-container--open .select2-dropdown--below {
|
||||
margin-top: -1px;
|
||||
border-top: 1px solid @field-border;
|
||||
}
|
||||
|
||||
.Form-dropDown:focus{
|
||||
|
||||
@ -214,7 +214,7 @@
|
||||
}
|
||||
|
||||
#job-detail-container {
|
||||
|
||||
|
||||
.well {
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -276,6 +276,8 @@
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: @white;
|
||||
min-height: 40px;
|
||||
|
||||
.row {
|
||||
border-top: 1px solid @grey;
|
||||
}
|
||||
@ -318,7 +320,7 @@
|
||||
|
||||
#play-section {
|
||||
.table-detail {
|
||||
height: 150px;
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -421,7 +423,6 @@
|
||||
table-layout: fixed;
|
||||
}
|
||||
#hosts-table-detail {
|
||||
height: 150px;
|
||||
background-color: @white;
|
||||
}
|
||||
#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';
|
||||
var module = ng.module('lrInfiniteScroll', []);
|
||||
|
||||
module.directive('lrInfiniteScroll', ['$log', '$timeout', function ($log, timeout) {
|
||||
module.directive('lrInfiniteScroll', ['$timeout', function (timeout) {
|
||||
return{
|
||||
link: function (scope, element, attr) {
|
||||
var
|
||||
lengthThreshold = attr.scrollThreshold || 50,
|
||||
timeThreshold = attr.timeThreshold || 400,
|
||||
direction = attr.direction || 'down',
|
||||
handler = scope.$eval(attr.lrInfiniteScroll),
|
||||
promise = null,
|
||||
lastRemaining = 9999;
|
||||
@ -20,14 +19,14 @@
|
||||
handler = ng.noop;
|
||||
}
|
||||
|
||||
$log.debug('lrInfiniteScroll: ' + attr.lrInfiniteScroll);
|
||||
|
||||
element.bind('scroll', function () {
|
||||
var remaining = (direction === 'down') ? element[0].scrollHeight - (element[0].clientHeight + element[0].scrollTop) : element[0].scrollTop;
|
||||
// if we have reached the threshold and we scroll down
|
||||
if ((direction === 'down' && remaining < lengthThreshold && (remaining - lastRemaining) < 0) ||
|
||||
direction === 'up' && remaining < lengthThreshold) {
|
||||
//if there is already a timer running which has not expired yet we have to cancel it and restart the timer
|
||||
var
|
||||
remaining = element[0].scrollHeight - (element[0].clientHeight + element[0].scrollTop);
|
||||
|
||||
//if we have reached the threshold and we scroll down
|
||||
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) {
|
||||
timeout.cancel(promise);
|
||||
}
|
||||
|
||||
@ -15,13 +15,13 @@
|
||||
multiSelectExtended: true,
|
||||
index: false,
|
||||
hover: true,
|
||||
|
||||
emptyListText : 'No Teams exist',
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
label: 'name'
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
multiSelectExtended: true,
|
||||
index: false,
|
||||
hover: true,
|
||||
emptyListText : 'No Users exist',
|
||||
|
||||
fields: {
|
||||
first_name: {
|
||||
|
||||
@ -23,7 +23,7 @@ export default
|
||||
return i.role;
|
||||
}))
|
||||
.filter((role) => {
|
||||
return !!attrs.teamRoleList == !!role.team_id;
|
||||
return Boolean(attrs.teamRoleList) === Boolean(role.team_id);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.name
|
||||
|
||||
@ -79,6 +79,7 @@ __deferLoadIfEnabled();
|
||||
|
||||
var tower = angular.module('Tower', [
|
||||
//'ngAnimate',
|
||||
'lrInfiniteScroll',
|
||||
'ngSanitize',
|
||||
'ngCookies',
|
||||
about.name,
|
||||
@ -269,7 +270,7 @@ var tower = angular.module('Tower', [
|
||||
}).
|
||||
|
||||
state('projects', {
|
||||
url: '/projects',
|
||||
url: '/projects?{status}',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: ProjectsList,
|
||||
data: {
|
||||
@ -297,6 +298,10 @@ var tower = angular.module('Tower', [
|
||||
controller: ProjectsEdit,
|
||||
data: {
|
||||
activityStreamId: 'id'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'projects',
|
||||
label: 'EDIT PROJECT'
|
||||
}
|
||||
}).
|
||||
state('projectOrganizations', {
|
||||
@ -340,6 +345,10 @@ var tower = angular.module('Tower', [
|
||||
controller: TeamsEdit,
|
||||
data: {
|
||||
activityStreamId: 'team_id'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: "teams",
|
||||
label: "EDIT TEAM"
|
||||
}
|
||||
}).
|
||||
|
||||
|
||||
@ -74,7 +74,7 @@ export default
|
||||
var licenseInfo = FeaturesService.getLicenseInfo();
|
||||
scope.licenseType = licenseInfo ? licenseInfo.license_type : null;
|
||||
if (!licenseInfo) {
|
||||
console.warn("License info not loaded correctly");
|
||||
console.warn("License info not loaded correctly"); // jshint ignore:line
|
||||
}
|
||||
})
|
||||
.catch(function (response) {
|
||||
|
||||
@ -22,7 +22,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
|
||||
Wait('start');
|
||||
|
||||
var list = ProjectList,
|
||||
defaultUrl = GetBasePath('projects'),
|
||||
defaultUrl = GetBasePath('projects') + ($stateParams.status ? '?status=' + $stateParams.status : ''),
|
||||
view = GenerateList,
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
mode = (base === 'projects') ? 'edit' : 'select',
|
||||
|
||||
@ -162,7 +162,7 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log,
|
||||
generator.reset();
|
||||
|
||||
$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));
|
||||
|
||||
CreateSelect2({
|
||||
@ -271,7 +271,7 @@ export function UsersEdit($scope, $rootScope, $location,
|
||||
generator.reset();
|
||||
|
||||
$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));
|
||||
|
||||
var setScopeFields = function(data){
|
||||
|
||||
@ -49,7 +49,7 @@ export default
|
||||
label: "Inventories",
|
||||
},
|
||||
{
|
||||
url: "/#/inventories/?inventory_sources_with_failures",
|
||||
url: "/#/inventories?status=sync-failed",
|
||||
number: scope.data.inventories.inventory_failed,
|
||||
label: "Inventory Sync Failures",
|
||||
isFailureCount: true
|
||||
@ -60,7 +60,7 @@ export default
|
||||
label: "Projects"
|
||||
},
|
||||
{
|
||||
url: "/#/projects/?status=failed",
|
||||
url: "/#/projects?status=failed",
|
||||
number: scope.data.projects.failed,
|
||||
label: "Project Sync Failures",
|
||||
isFailureCount: true
|
||||
|
||||
@ -118,6 +118,7 @@
|
||||
top: auto;
|
||||
box-shadow: none;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.DashboardGraphs-periodDropdown,
|
||||
|
||||
@ -246,7 +246,7 @@ export default
|
||||
rows: 10,
|
||||
awPopOver: "SSH key description",
|
||||
awPopOverWatch: "key_description",
|
||||
dataTitle: 'Help',
|
||||
dataTitle: 'Private Key',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
subForm: "credentialSubForm"
|
||||
|
||||
@ -204,7 +204,7 @@ export default
|
||||
},
|
||||
job_tags: {
|
||||
label: 'Job Tags',
|
||||
type: 'textarea',
|
||||
type: 'text',
|
||||
rows: 1,
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
|
||||
@ -174,7 +174,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
dataTitle: 'SCM Clean',
|
||||
dataContainer: 'body',
|
||||
dataPlacement: 'right',
|
||||
labelClass: 'checkbox-options'
|
||||
labelClass: 'checkbox-options stack-inline'
|
||||
}, {
|
||||
name: 'scm_delete_on_update',
|
||||
label: 'Delete on Update',
|
||||
@ -186,7 +186,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
dataTitle: 'SCM Delete',
|
||||
dataContainer: 'body',
|
||||
dataPlacement: 'right',
|
||||
labelClass: 'checkbox-options'
|
||||
labelClass: 'checkbox-options stack-inline'
|
||||
}, {
|
||||
name: 'scm_update_on_launch',
|
||||
label: 'Update on Launch',
|
||||
@ -197,7 +197,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
dataTitle: 'SCM Update',
|
||||
dataContainer: 'body',
|
||||
dataPlacement: 'right',
|
||||
labelClass: 'checkbox-options'
|
||||
labelClass: 'checkbox-options stack-inline'
|
||||
}]
|
||||
},
|
||||
scm_update_cache_timeout: {
|
||||
@ -273,7 +273,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
}
|
||||
},
|
||||
notifications: {
|
||||
include: "NotificationsList"
|
||||
include: "NotificationsList",
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -132,9 +132,10 @@ export default
|
||||
"delete": {
|
||||
label: 'Remove',
|
||||
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',
|
||||
awToolTip: 'Dissasociate permission from team'
|
||||
awToolTip: 'Dissasociate permission from team',
|
||||
dataPlacement: 'top'
|
||||
}
|
||||
},
|
||||
hideOnSuperuser: true
|
||||
|
||||
@ -138,6 +138,7 @@ export default
|
||||
iterator: 'team',
|
||||
open: false,
|
||||
index: false,
|
||||
suppressEmptyText: true,
|
||||
|
||||
actions: {},
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ export default
|
||||
angular.module('SchedulesHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', 'SearchHelper', 'PaginationHelpers', listGenerator.name, 'ModalDialog',
|
||||
'GeneratorHelpers'])
|
||||
|
||||
.factory('EditSchedule', ['SchedulerInit', '$rootScope', 'Wait', 'Rest',
|
||||
.factory('EditSchedule', ['SchedulerInit', '$rootScope', 'Wait', 'Rest',
|
||||
'ProcessErrors', 'GetBasePath', 'SchedulePost', '$state',
|
||||
function(SchedulerInit, $rootScope, Wait, Rest, ProcessErrors,
|
||||
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',
|
||||
function($location, $rootScope, $stateParams, SchedulerInit,
|
||||
Wait, GetBasePath, Empty, SchedulePost, $state, Rest,
|
||||
@ -295,8 +295,7 @@ export default
|
||||
}])
|
||||
|
||||
.factory('SchedulePost', ['Rest', 'ProcessErrors', 'RRuleToAPI', 'Wait',
|
||||
'ToJSON',
|
||||
function(Rest, ProcessErrors, RRuleToAPI, Wait, ToJSON) {
|
||||
function(Rest, ProcessErrors, RRuleToAPI, Wait) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
url = params.url,
|
||||
@ -326,8 +325,8 @@ export default
|
||||
schedule.extra_data = JSON.stringify(extra_vars);
|
||||
}
|
||||
else if(scope.extraVars){
|
||||
schedule.extra_data = scope.parseType === 'yaml' ?
|
||||
(scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars;
|
||||
schedule.extra_data = scope.parseType === 'yaml' ?
|
||||
(scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars;
|
||||
}
|
||||
Rest.setUrl(url);
|
||||
if (mode === 'add') {
|
||||
|
||||
@ -17,7 +17,7 @@ function InventoriesList($scope, $rootScope, $location, $log,
|
||||
Find, Empty, $state) {
|
||||
|
||||
var list = InventoryList,
|
||||
defaultUrl = GetBasePath('inventory'),
|
||||
defaultUrl = GetBasePath('inventory') + ($stateParams.status === 'sync-failed' ? '?not__inventory_sources_with_failures=0' : ''),
|
||||
view = generateList,
|
||||
paths = $location.path().replace(/^\//, '').split('/'),
|
||||
mode = (paths[0] === 'inventories') ? 'edit' : 'select';
|
||||
|
||||
@ -9,7 +9,7 @@ import InventoriesList from './inventory-list.controller';
|
||||
|
||||
export default {
|
||||
name: 'inventories',
|
||||
route: '/inventories',
|
||||
route: '/inventories?{status}',
|
||||
templateUrl: templateUrl('inventories/inventories'),
|
||||
controller: InventoriesList,
|
||||
data: {
|
||||
|
||||
@ -80,7 +80,7 @@
|
||||
group_id: id
|
||||
});
|
||||
};
|
||||
$scope.showFailedHosts = function(x, y, z){
|
||||
$scope.showFailedHosts = function() {
|
||||
$state.go('inventoryManage', {failed: true}, {reload: true});
|
||||
};
|
||||
$scope.scheduleGroup = function(id) {
|
||||
@ -91,7 +91,7 @@
|
||||
$scope.$parent.groupsSelected = selection.length > 0 ? true : false;
|
||||
$scope.$parent.groupsSelectedItems = selection.selectedItems;
|
||||
});
|
||||
$scope.$on('PostRefresh', () =>{
|
||||
$scope.$on('PostRefresh', () => {
|
||||
$scope.groups.forEach( (group, index) => {
|
||||
var group_status, hosts_status;
|
||||
group_status = GetSyncStatusMsg({
|
||||
|
||||
@ -8,7 +8,7 @@ import {ManageHostsAdd, ManageHostsEdit} from './hosts.route';
|
||||
|
||||
export default
|
||||
angular.module('manageHosts', [])
|
||||
.run(['$stateExtender', '$state', function($stateExtender, $state){
|
||||
.run(['$stateExtender', function($stateExtender){
|
||||
$stateExtender.addState(ManageHostsAdd);
|
||||
$stateExtender.addState(ManageHostsEdit);
|
||||
}]);
|
||||
|
||||
@ -61,7 +61,7 @@ export default
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
}
|
||||
}
|
||||
$scope.canEdit = data['script'] !== null;
|
||||
$scope.canEdit = data.script !== null;
|
||||
if (!$scope.canEdit) {
|
||||
$scope.script = "Script contents hidden";
|
||||
}
|
||||
|
||||
@ -33,8 +33,8 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="hosts-summary-table" class="table-detail" lr-infinite-scroll="getNextPage" scroll-threshold="10" time-threshold="500" ng-hide="hosts.length == 0">
|
||||
<table class="table">
|
||||
<div id="hosts-summary-table" class="table-detail" lr-infinite-scroll="getNextPage" scroll-threshold="10" time-threshold="500">
|
||||
<table class="table" ng-class="{'JobDetails-table--noResults': hosts.length === 0}">
|
||||
<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'">
|
||||
<td class="List-tableCell name col-lg-6 col-md-6 col-sm-6 col-xs-6">
|
||||
|
||||
@ -13,6 +13,12 @@
|
||||
.JobDetail-instructions{
|
||||
color: @default-interface-txt;
|
||||
margin: 10px 0 10px 0;
|
||||
|
||||
.badge {
|
||||
background-color: @default-list-header-bg;
|
||||
color: @default-interface-txt;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
}
|
||||
.JobDetail{
|
||||
.OnePlusOne-container(100%, @breakpoint-md);
|
||||
@ -151,6 +157,10 @@
|
||||
background-color: @default-link;
|
||||
border: 1px solid @default-link;
|
||||
color: @default-bg;
|
||||
|
||||
&:hover {
|
||||
background-color: @default-link-hov;
|
||||
}
|
||||
}
|
||||
.JobDetail .nvd3.nv-noData{
|
||||
color: @default-interface-txt;
|
||||
@ -198,6 +208,12 @@
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.JobDetails-table--noResults {
|
||||
tr > td {
|
||||
border-top: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.JobDetail-statusIcon--results{
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
@ -215,3 +231,7 @@
|
||||
width: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
|
||||
|
||||
// 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() -
|
||||
$('#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');
|
||||
};
|
||||
|
||||
|
||||
@ -189,7 +189,7 @@
|
||||
</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">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -202,7 +202,7 @@
|
||||
</div>
|
||||
<div id="plays-table-detail" class="table-detail" lr-infinite-scroll="playsScrollDown"
|
||||
scroll-threshold="10" time-threshold="500">
|
||||
<table class="table">
|
||||
<table class="table" ng-class="{'JobDetails-table--noResults': plays.length === 0}">
|
||||
<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)">
|
||||
<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 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>
|
||||
<tr>
|
||||
<th class="List-tableHeader col-lg-3 col-md-3 col-sm-6 col-xs-4">Tasks</th>
|
||||
@ -261,7 +261,7 @@
|
||||
</div>
|
||||
<div id="tasks-table-detail" class="table-detail" lr-infinite-scroll="tasksScrollDown"
|
||||
scroll-threshold="10" time-threshold="500">
|
||||
<table class="table">
|
||||
<table class="table" ng-class="{'JobDetails-table--noResults': taskList.length === 0}">
|
||||
<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)">
|
||||
<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 class="table-header" id="hosts-table-header">
|
||||
<table class="table table-condensed">
|
||||
<table class="table table-condensed" ng-show="results.length !== 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="List-tableHeader col-lg-4 col-md-3 col-sm-3 col-xs-3">Hosts</th>
|
||||
@ -343,7 +343,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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">
|
||||
@ -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()">
|
||||
<i class="fa fa-arrows-alt"></i>
|
||||
</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">
|
||||
<i class="fa fa-download"></i>
|
||||
</button>
|
||||
|
||||
@ -72,6 +72,7 @@
|
||||
jQuery.extend(true, CloudCredentialList, CredentialList);
|
||||
CloudCredentialList.name = 'cloudcredentials';
|
||||
CloudCredentialList.iterator = 'cloudcredential';
|
||||
CloudCredentialList.basePath = '/api/v1/credentials?cloud=true';
|
||||
|
||||
SurveyControllerInit({
|
||||
scope: $scope,
|
||||
|
||||
@ -108,7 +108,7 @@ export default
|
||||
}
|
||||
})
|
||||
.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. */
|
||||
} else {
|
||||
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) {
|
||||
if (status == 403) {
|
||||
if (status === 403) {
|
||||
/* User doesn't have read access to the project, no problem. */
|
||||
} else {
|
||||
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);
|
||||
CloudCredentialList.name = 'cloudcredentials';
|
||||
CloudCredentialList.iterator = 'cloudcredential';
|
||||
CloudCredentialList.basePath = '/api/v1/credentials?cloud=true';
|
||||
LookUpInit({
|
||||
url: GetBasePath('credentials') + '?cloud=true',
|
||||
scope: $scope,
|
||||
|
||||
@ -20,6 +20,7 @@ export default
|
||||
index: false,
|
||||
hover: true,
|
||||
well: false,
|
||||
emptyListText: 'No completed jobs',
|
||||
|
||||
fields: {
|
||||
status: {
|
||||
|
||||
@ -15,6 +15,7 @@ export default
|
||||
index: true,
|
||||
hover: true,
|
||||
well: false,
|
||||
emptyListText: 'No schedules exist',
|
||||
|
||||
fields: {
|
||||
status: {
|
||||
|
||||
@ -157,11 +157,11 @@
|
||||
ng-href="/#/portal"
|
||||
ng-if="!licenseMissing"
|
||||
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-trigger="hover"
|
||||
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">
|
||||
</i>
|
||||
</a>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
<div ui-view></div>
|
||||
<div class="tab-pane Panel" id="management_jobs">
|
||||
<div class="List-title">
|
||||
<div class="List-titleText">
|
||||
@ -7,7 +8,6 @@
|
||||
{{ mgmtCards.length }}
|
||||
</span>
|
||||
</div>
|
||||
<div ui-view></div>
|
||||
<div class="MgmtCards">
|
||||
<div class="MgmtCards-card"
|
||||
ng-repeat="card in mgmtCards track by card.id">
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
|
||||
.MgmtCards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.MgmtCards-card {
|
||||
@ -11,15 +12,14 @@
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid @default-border;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
margin-top: 20px;
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
.MgmtCards-card--selected {
|
||||
padding-left: 16px;
|
||||
border-left: 5px solid #337AB7;
|
||||
border-left: 5px solid @default-link;
|
||||
}
|
||||
|
||||
.MgmtCards-card--promptElements{
|
||||
@ -44,7 +44,6 @@
|
||||
font-weight: bold;
|
||||
color: @default-interface-txt;
|
||||
margin-bottom: 25px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@ -86,46 +85,19 @@
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 1179px) {
|
||||
.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) {
|
||||
@media (max-width: 840px) {
|
||||
.MgmtCards-card {
|
||||
width: 100%;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 840px) and (max-width: 1240px) {
|
||||
.MgmtCards-card {
|
||||
width: ~"calc(50% - 10px)";
|
||||
}
|
||||
}
|
||||
|
||||
#prompt-for-days-facts, #prompt-for-days {
|
||||
overflow-x: hidden;
|
||||
font-family: "Open Sans";
|
||||
|
||||
@ -141,7 +141,23 @@ export default function() {
|
||||
reqExpression: "channel_required",
|
||||
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'
|
||||
},
|
||||
token: {
|
||||
@ -243,8 +259,9 @@ export default function() {
|
||||
subForm: 'typeSubForm'
|
||||
},
|
||||
api_url: {
|
||||
label: 'API URL (e.g: https://mycompany.hiptchat.com)',
|
||||
label: 'API URL',
|
||||
type: 'text',
|
||||
placeholder: 'https://mycompany.hipchat.com',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "hipchat_required",
|
||||
init: "false"
|
||||
@ -264,11 +281,7 @@ export default function() {
|
||||
},
|
||||
notify: {
|
||||
label: 'Notify Channel',
|
||||
type: 'text',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "hipchat_required",
|
||||
init: "false"
|
||||
},
|
||||
type: 'checkbox',
|
||||
ngShow: "notification_type.value == 'hipchat' ",
|
||||
subForm: 'typeSubForm'
|
||||
},
|
||||
|
||||
@ -13,6 +13,7 @@ export default function(){
|
||||
iterator: 'notification_template',
|
||||
index: false,
|
||||
hover: false,
|
||||
emptyListText: 'No notifications exist',
|
||||
|
||||
fields: {
|
||||
status: {
|
||||
|
||||
@ -14,6 +14,7 @@ export default function(){
|
||||
iterator: 'notification',
|
||||
index: false,
|
||||
hover: false,
|
||||
emptyListText: 'No Notifications exist',
|
||||
basePath: 'notifications',
|
||||
fields: {
|
||||
name: {
|
||||
|
||||
@ -39,7 +39,7 @@ function () {
|
||||
case 'hipchat':
|
||||
obj.tokenLabel = ' Token';
|
||||
obj.hipchat_required = true;
|
||||
obj.channel_required = true;
|
||||
obj.room_required = true;
|
||||
obj.token_required = true;
|
||||
break;
|
||||
case 'twilio':
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<div id="scheduled_jobs_link" class="Form-tab"
|
||||
ng-class="{'is-selected': schedulesSelected }"
|
||||
ng-click="toggleTab('scheduled')">
|
||||
Schedule
|
||||
Schedules
|
||||
</div>
|
||||
</div>
|
||||
<div id="jobs-tab-content" class="Form-tabSection"
|
||||
|
||||
@ -8,7 +8,7 @@ export default {
|
||||
name: 'portalMode',
|
||||
url: '/portal',
|
||||
ncyBreadcrumb: {
|
||||
label: "PORTAL MODE"
|
||||
label: "MY VIEW"
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
@ -30,4 +30,4 @@ export default {
|
||||
controller: PortalModeJobsController
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -37,19 +37,28 @@ export default
|
||||
name: 'projectSchedules',
|
||||
route: '/projects/:id/schedules',
|
||||
templateUrl: templateUrl("scheduler/scheduler"),
|
||||
controller: 'schedulerController'
|
||||
controller: 'schedulerController',
|
||||
ncyBreadcrumb: {
|
||||
label: 'PROJECT SCHEDULES'
|
||||
}
|
||||
});
|
||||
$stateExtender.addState({
|
||||
name: 'projectSchedules.add',
|
||||
route: '/add',
|
||||
templateUrl: templateUrl("scheduler/schedulerForm"),
|
||||
controller: 'schedulerAddController'
|
||||
controller: 'schedulerAddController',
|
||||
ncyBreadcrumb: {
|
||||
label: 'PROJECT SCHEDULES ADD'
|
||||
}
|
||||
});
|
||||
$stateExtender.addState({
|
||||
name: 'projectSchedules.edit',
|
||||
route: '/:schedule_id',
|
||||
templateUrl: templateUrl("scheduler/schedulerForm"),
|
||||
controller: 'schedulerEditController'
|
||||
controller: 'schedulerEditController',
|
||||
ncyBreadcrumb: {
|
||||
label: 'PROJECT SCHEDULES EDIT'
|
||||
}
|
||||
});
|
||||
$stateExtender.addState({
|
||||
name: 'inventoryManage.schedules',
|
||||
|
||||
@ -509,7 +509,7 @@
|
||||
Please provide a valid date.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group SchedulerForm-formGroup"
|
||||
<div class="form-group SchedulerForm-formGroup"
|
||||
ng-if="schedulerEnd && schedulerEnd.value == 'on'">
|
||||
<label class="Form-inputLabel">
|
||||
<span class="red-text">*</span>
|
||||
@ -583,7 +583,7 @@
|
||||
<div class="SchedulerFormDetail-container"
|
||||
ng-show="schedulerIsValid">
|
||||
<label class="SchedulerFormDetail-label">
|
||||
Description
|
||||
Schedule Description
|
||||
</label>
|
||||
<div class="SchedulerFormDetail-nlp">
|
||||
{{ rrule_nlp_description }}
|
||||
@ -651,7 +651,7 @@
|
||||
<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">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
<div class="parse-selection">
|
||||
<input type="radio" ng-model="parseType" ng-change="parseTypeChange()" value="yaml"><span class="parse-label">YAML</span>
|
||||
|
||||
@ -31,7 +31,6 @@
|
||||
align-items: center;
|
||||
max-height: 400px;
|
||||
width: 120px;
|
||||
overflow-y: scroll;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ export default {
|
||||
name: 'setup',
|
||||
route: '/setup',
|
||||
ncyBreadcrumb: {
|
||||
label: "SETUP"
|
||||
label: "SETTINGS"
|
||||
},
|
||||
templateUrl: templateUrl('setup-menu/setup-menu')
|
||||
};
|
||||
|
||||
@ -59,7 +59,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
|
||||
callback = params.callback,
|
||||
beforeDestroy = params.beforeDestroy,
|
||||
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,
|
||||
dialogClass = params.dialogClass,
|
||||
forms = _.chain([params.form]).flatten().compact().value(),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// default base colors
|
||||
@default-interface-txt: #848992;
|
||||
@default-interface-txt: #707070;
|
||||
@default-data-txt: #161B1F;
|
||||
@default-icon: #B7B7B7;
|
||||
@default-icon: #848992;
|
||||
@default-icon-hov: #D7D7D7; // also selected button
|
||||
@default-border: #E8E8E8;
|
||||
@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-dark: #000000;
|
||||
|
||||
|
||||
@default-warning: #F0AD4E;
|
||||
@default-warning-hov: #EC971F;
|
||||
@default-unreachable: #FF0000;
|
||||
@ -35,7 +36,7 @@
|
||||
|
||||
// buttons
|
||||
@btn-bg: @default-bg;
|
||||
@btn-bord: @default-border;
|
||||
@btn-bord: @d7grey;
|
||||
@btn-txt: @default-interface-txt;
|
||||
@btn-bg-hov: @default-tertiary-bg;
|
||||
@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
|
||||
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 += `
|
||||
<div class=\"List-noItems\" ng-show=\"is_superuser\">
|
||||
|
||||
@ -87,7 +87,7 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
|
||||
});
|
||||
|
||||
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()
|
||||
.success(function(data) {
|
||||
Wait('stop');
|
||||
@ -145,38 +145,17 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
|
||||
});
|
||||
}
|
||||
|
||||
$scope.stdOutScrollToTop = function() {
|
||||
// scroll up or back in time toward the beginning of the file
|
||||
var start, end, url;
|
||||
if (loaded_sections.length > 0 && loaded_sections[0].start > 0) {
|
||||
start = (loaded_sections[0].start - page_size > 0) ? loaded_sections[0].start - page_size : 0;
|
||||
end = loaded_sections[0].start - 1;
|
||||
}
|
||||
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;
|
||||
// lrInfiniteScroll handler
|
||||
// grabs the next stdout section
|
||||
$scope.stdOutGetNextSection = function(){
|
||||
if (current_range.absolute_end > current_range.end){
|
||||
var url = $scope.stdoutEndpoint + '?format=json&start_line=' + current_range.end +
|
||||
'&end_line=' + (current_range.end + page_size);
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success( function(data) {
|
||||
//var currentPos = $('#pre-container').scrollTop();
|
||||
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
|
||||
});
|
||||
.success(function(data){
|
||||
$('#pre-container-content').append(data.content);
|
||||
current_range = data.range;
|
||||
$('#stdoutMoreRowsTop').fadeOut(400);
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<div class="StandardOut-consoleOutput">
|
||||
<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 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-content" class="StandardOut-preContent"></div>
|
||||
</div>
|
||||
<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 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 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 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/*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user