mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 01:57:35 -03:30
Merge branch 'release_3.0.0' into devel
* release_3.0.0: (129 commits) Use the correct yum attribute name Fix NaN / invalid end date value in schedule edit forms (#3007) resolves kickback on #2976 (#3009) Additional UI/JS licenses (#2974) Install at when configuring on-book AMI conf Added patch tests for updating project organizations Make development environment use ATOMIC_REQUESTS Team organization field made non-null Don't let normal users create orgless projects Fix up some flake8 issues Add delete protection from certain objects Cascade delete teams when their organization is deleted Updated tests to reflect credential access after migrations Changed conditonal to repair console error and page loade error Remove socket binding when the scope is destroyed so that we don't make errant GET requests in future instances of this controller. updated EULA text from rnalen Fix team credential role access in rbac migration Orphan handling in _old_access.py Normalized CustomInventoryScriptAccess.can_admin Use simplified epel-release links ...
This commit is contained in:
commit
ba444ab743
@ -87,6 +87,7 @@ SUMMARIZABLE_FK_FIELDS = {
|
||||
'current_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'),
|
||||
'current_job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'),
|
||||
'inventory_source': ('source', 'last_updated', 'status'),
|
||||
'custom_inventory_script': DEFAULT_SUMMARY_FIELDS,
|
||||
'source_script': ('name', 'description'),
|
||||
'role': ('id', 'role_field'),
|
||||
'notification_template': DEFAULT_SUMMARY_FIELDS,
|
||||
@ -917,6 +918,19 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
|
||||
args=(obj.last_update.pk,))
|
||||
return res
|
||||
|
||||
def validate(self, attrs):
|
||||
organization = None
|
||||
if 'organization' in attrs:
|
||||
organization = attrs['organization']
|
||||
elif self.instance:
|
||||
organization = self.instance.organization
|
||||
|
||||
view = self.context.get('view', None)
|
||||
if not organization and not view.request.user.is_superuser:
|
||||
# Only allow super users to create orgless projects
|
||||
raise serializers.ValidationError('Organization is missing')
|
||||
return super(ProjectSerializer, self).validate(attrs)
|
||||
|
||||
|
||||
class ProjectPlaybooksSerializer(ProjectSerializer):
|
||||
|
||||
@ -1926,12 +1940,11 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
def to_internal_value(self, data):
|
||||
# When creating a new job and a job template is specified, populate any
|
||||
# fields not provided in data from the job template.
|
||||
if not self.instance and isinstance(data, dict) and 'job_template' in data:
|
||||
if not self.instance and isinstance(data, dict) and data.get('job_template', False):
|
||||
try:
|
||||
job_template = JobTemplate.objects.get(pk=data['job_template'])
|
||||
except JobTemplate.DoesNotExist:
|
||||
self._errors = {'job_template': 'Invalid job template.'}
|
||||
return
|
||||
raise serializers.ValidationError({'job_template': 'Invalid job template.'})
|
||||
data.setdefault('name', job_template.name)
|
||||
data.setdefault('description', job_template.description)
|
||||
data.setdefault('job_type', job_template.job_type)
|
||||
@ -2427,14 +2440,10 @@ class NotificationTemplateSerializer(BaseSerializer):
|
||||
notification_type = attrs['notification_type']
|
||||
elif self.instance:
|
||||
notification_type = self.instance.notification_type
|
||||
if 'organization' in attrs:
|
||||
organization = attrs['organization']
|
||||
elif self.instance:
|
||||
organization = self.instance.organization
|
||||
else:
|
||||
notification_type = None
|
||||
if not notification_type:
|
||||
raise serializers.ValidationError('Missing required fields for Notification Configuration: notification_type')
|
||||
if not organization:
|
||||
raise serializers.ValidationError("Missing 'organization' from required fields")
|
||||
|
||||
notification_class = NotificationTemplate.CLASS_FOR_NOTIFICATION_TYPE[notification_type]
|
||||
missing_fields = []
|
||||
@ -2621,7 +2630,11 @@ class ActivityStreamSerializer(BaseSerializer):
|
||||
if getattr(obj, fk).exists():
|
||||
rel[fk] = []
|
||||
for thisItem in allm2m:
|
||||
rel[fk].append(reverse('api:' + fk + '_detail', args=(thisItem.id,)))
|
||||
if fk == 'custom_inventory_script':
|
||||
rel[fk].append(reverse('api:inventory_script_detail', args=(thisItem.id,)))
|
||||
else:
|
||||
rel[fk].append(reverse('api:' + fk + '_detail', args=(thisItem.id,)))
|
||||
|
||||
if fk == 'schedule':
|
||||
rel['unified_job_template'] = thisItem.unified_job_template.get_absolute_url()
|
||||
return rel
|
||||
@ -2694,8 +2707,7 @@ class TowerSettingsSerializer(BaseSerializer):
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if data['key'] not in settings.TOWER_SETTINGS_MANIFEST:
|
||||
self._errors = {'key': 'Key {0} is not a valid settings key.'.format(data['key'])}
|
||||
return
|
||||
raise serializers.ValidationError({'key': ['Key {0} is not a valid settings key.'.format(data['key'])]})
|
||||
ret = super(TowerSettingsSerializer, self).to_internal_value(data)
|
||||
manifest_val = settings.TOWER_SETTINGS_MANIFEST[data['key']]
|
||||
ret['description'] = manifest_val['description']
|
||||
|
||||
9
awx/api/templates/api/job_template_label_list.md
Normal file
9
awx/api/templates/api/job_template_label_list.md
Normal file
@ -0,0 +1,9 @@
|
||||
{% include "api/sub_list_create_api_view.md" %}
|
||||
|
||||
Labels not associated with any other resources are deleted. A label can become disassociated with a resource as a result of 3 events.
|
||||
|
||||
1. A label is explicitly diassociated with a related job template
|
||||
2. A job is deleted with labels
|
||||
3. A cleanup job deletes a job with labels
|
||||
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
@ -34,7 +34,7 @@ existing {{ model_verbose_name }} with this {{ parent_model_verbose_name }}.
|
||||
|
||||
Make a POST request to this resource with `id` and `disassociate` fields to
|
||||
remove the {{ model_verbose_name }} from this {{ parent_model_verbose_name }}
|
||||
without deleting the {{ model_verbose_name }}.
|
||||
{% if model_verbose_name != "label" %} without deleting the {{ model_verbose_name }}{% endif %}.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@ -1,3 +1,19 @@
|
||||
TOWER SOFTWARE END USER LICENSE AGREEMENT
|
||||
ANSIBLE TOWER BY RED HAT END USER LICENSE AGREEMENT
|
||||
|
||||
Unless otherwise agreed to, and executed in a definitive agreement, between Ansible, Inc. (“Ansible”) and the individual or entity (“Customer”) signing or electronically accepting these terms of use for the Tower Software (“EULA”), all Tower Software, including any and all versions released or made available by Ansible, shall be subject to the Ansible Software Subscription and Services Agreement found at www.ansible.com/subscription-agreement (“Agreement”). Ansible is not responsible for any additional obligations, conditions or warranties agreed to between Customer and an authorized distributor, or reseller, of the Tower Software. BY DOWNLOADING AND USING THE TOWER SOFTWARE, OR BY CLICKING ON THE “YES” BUTTON OR OTHER BUTTON OR MECHANISM DESIGNED TO ACKNOWLEDGE CONSENT TO THE TERMS OF AN ELECTRONIC COPY OF THIS EULA, THE CUSTOMER HEREBY ACKNOWLEDGES THAT CUSTOMER HAS READ, UNDERSTOOD, AND AGREES TO BE BOUND BY THE TERMS OF THIS EULA AND AGREEMENT, INCLUDING ALL TERMS INCORPORATED HEREIN BY REFERENCE, AND THAT THIS EULA AND AGREEMENT IS EQUIVALENT TO ANY WRITTEN NEGOTIATED AGREEMENT BETWEEN CUSTOMER AND ANSIBLE. THIS EULA AND AGREEMENT IS ENFORCEABLE AGAINST ANY PERSON OR ENTITY THAT USES OR AVAILS ITSELF OF THE TOWER SOFTWARE OR ANY PERSON OR ENTITY THAT USES THE OR AVAILS ITSELF OF THE TOWER SOFTWARE ON ANOTHER PERSON’S OR ENTITY’S BEHALF.
|
||||
This end user license agreement (“EULA”) governs the use of the Ansible Tower software and any related updates, upgrades, versions, appearance, structure and organization (the “Ansible Tower Software”), regardless of the delivery mechanism.
|
||||
|
||||
1. License Grant. Subject to the terms of this EULA, Red Hat, Inc. and its affiliates (“Red Hat”) grant to you (“You”) a non-transferable, non-exclusive, worldwide, non-sublicensable, limited, revocable license to use the Ansible Tower Software for the term of the associated Red Hat Software Subscription(s) and in a quantity equal to the number of Red Hat Software Subscriptions purchased from Red Hat for the Ansible Tower Software (“License”), each as set forth on the applicable Red Hat ordering document. You acquire only the right to use the Ansible Tower Software and do not acquire any rights of ownership. Red Hat reserves all rights to the Ansible Tower Software not expressly granted to You. This License grant pertains solely to Your use of the Ansible Tower Software and is not intended to limit Your rights under, or grant You rights that supersede, the license terms of any software packages which may be made available with the Ansible Tower Software that are subject to an open source software license.
|
||||
|
||||
2. Intellectual Property Rights. Title to the Ansible Tower Software and each component, copy and modification, including all derivative works whether made by Red Hat, You or on Red Hat's behalf, including those made at Your suggestion and all associated intellectual property rights, are and shall remain the sole and exclusive property of Red Hat and/or it licensors. The License does not authorize You (nor may You allow any third party, specifically non-employees of Yours) to: (a) copy, distribute, reproduce, use or allow third party access to the Ansible Tower Software except as expressly authorized hereunder; (b) decompile, disassemble, reverse engineer, translate, modify, convert or apply any procedure or process to the Ansible Tower Software in order to ascertain, derive, and/or appropriate for any reason or purpose, including the Ansible Tower Software source code or source listings or any trade secret information or process contained in the Ansible Tower Software (except as permitted under applicable law); (c) execute or incorporate other software (except for approved software as appears in the Ansible Tower Software documentation or specifically approved by Red Hat in writing) into Ansible Tower Software, or create a derivative work of any part of the Ansible Tower Software; (d) remove any trademarks, trade names or titles, copyrights legends or any other proprietary marking on the Ansible Tower Software; (e) disclose the results of any benchmarking of the Ansible Tower Software (whether or not obtained with Red Hat’s assistance) to any third party; (f) attempt to circumvent any user limits or other license, timing or use restrictions that are built into, defined or agreed upon, regarding the Ansible Tower Software. You are hereby notified that the Ansible Tower Software may contain time-out devices, counter devices, and/or other devices intended to ensure the limits of the License will not be exceeded (“Limiting Devices”). If the Ansible Tower Software contains Limiting Devices, Red Hat will provide You materials necessary to use the Ansible Tower Software to the extent permitted. You may not tamper with or otherwise take any action to defeat or circumvent a Limiting Device or other control measure, including but not limited to, resetting the unit amount or using false host identification number for the purpose of extending any term of the License.
|
||||
|
||||
3. Evaluation Licenses. Unless You have purchased Ansible Tower Software Subscriptions from Red Hat or an authorized reseller under the terms of a commercial agreement with Red Hat, all use of the Ansible Tower Software shall be limited to testing purposes and not for production use (“Evaluation”). Unless otherwise agreed by Red Hat, Evaluation of the Ansible Tower Software shall be limited to an evaluation environment and the Ansible Tower Software shall not be used to manage any systems or virtual machines on networks being used in the operation of Your business or any other non-evaluation purpose. Unless otherwise agreed by Red Hat, You shall limit all Evaluation use to a single 30 day evaluation period and shall not download or otherwise obtain additional copies of the Ansible Tower Software or license keys for Evaluation.
|
||||
|
||||
4. Limited Warranty. Except as specifically stated in this Section 4, to the maximum extent permitted under applicable law, the Ansible Tower Software and the components are provided and licensed “as is” without warranty of any kind, expressed or implied, including the implied warranties of merchantability, non-infringement or fitness for a particular purpose. Red Hat warrants solely to You that the media on which the Ansible Tower Software may be furnished will be free from defects in materials and manufacture under normal use for a period of thirty (30) days from the date of delivery to You. Red Hat does not warrant that the functions contained in the Ansible Tower Software will meet Your requirements or that the operation of the Ansible Tower Software will be entirely error free, appear precisely as described in the accompanying documentation, or comply with regulatory requirements.
|
||||
|
||||
5. Limitation of Remedies and Liability. To the maximum extent permitted by applicable law, Your exclusive remedy under this EULA is to return any defective media within thirty (30) days of delivery along with a copy of Your payment receipt and Red Hat, at its option, will replace it or refund the money paid by You for the media. To the maximum extent permitted under applicable law, neither Red Hat nor any Red Hat authorized distributor will be liable to You for any incidental or consequential damages, including lost profits or lost savings arising out of the use or inability to use the Ansible Tower Software or any component, even if Red Hat or the authorized distributor has been advised of the possibility of such damages. In no event shall Red Hat's liability or an authorized distributor’s liability exceed the amount that You paid to Red Hat for the Ansible Tower Software during the twelve months preceding the first event giving rise to liability.
|
||||
|
||||
6. Export Control. In accordance with the laws of the United States and other countries, You represent and warrant that You: (a) understand that the Ansible Tower Software and its components may be subject to export controls under the U.S. Commerce Department’s Export Administration Regulations (“EAR”); (b) are not located in any country listed in Country Group E:1 in Supplement No. 1 to part 740 of the EAR; (c) will not export, re-export, or transfer the Ansible Tower Software to any prohibited destination or to any end user who has been prohibited from participating in US export transactions by any federal agency of the US government; (d) will not use or transfer the Ansible Tower Software for use in connection with the design, development or production of nuclear, chemical or biological weapons, or rocket systems, space launch vehicles, or sounding rockets or unmanned air vehicle systems; (e) understand and agree that if you are in the United States and you export or transfer the Ansible Tower Software to eligible end users, you will, to the extent required by EAR Section 740.17 obtain a license for such export or transfer and will submit semi-annual reports to the Commerce Department’s Bureau of Industry and Security, which include the name and address (including country) of each transferee; and (f) understand that countries including the United States may restrict the import, use, or export of encryption products (which may include the Ansible Tower Software) and agree that you shall be solely responsible for compliance with any such import, use, or export restrictions.
|
||||
|
||||
7. General. If any provision of this EULA is held to be unenforceable, that shall not affect the enforceability of the remaining provisions. This agreement shall be governed by the laws of the State of New York and of the United States, without regard to any conflict of laws provisions. The rights and obligations of the parties to this EULA shall not be governed by the United Nations Convention on the International Sale of Goods.
|
||||
|
||||
Copyright © 2015 Red Hat, Inc. All rights reserved. "Red Hat" and “Ansible Tower” are registered trademarks of Red Hat, Inc. All other trademarks are the property of their respective owners.
|
||||
|
||||
@ -958,6 +958,7 @@ class ProjectList(ListCreateAPIView):
|
||||
'admin_role',
|
||||
'use_role',
|
||||
'update_role',
|
||||
'read_role',
|
||||
)
|
||||
return projects_qs
|
||||
|
||||
@ -1401,7 +1402,7 @@ class OrganizationCredentialList(SubListCreateAPIView):
|
||||
user_visible = Credential.accessible_objects(self.request.user, 'read_role').all()
|
||||
org_set = Credential.accessible_objects(organization.admin_role, 'read_role').all()
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
if self.request.user.is_superuser or self.request.user.is_system_auditor:
|
||||
return org_set
|
||||
|
||||
return org_set & user_visible
|
||||
@ -1487,7 +1488,7 @@ class InventoryList(ListCreateAPIView):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Inventory.accessible_objects(self.request.user, 'read_role')
|
||||
qs = qs.select_related('admin_role', 'read_role', 'update_role', 'use_role')
|
||||
qs = qs.select_related('admin_role', 'read_role', 'update_role', 'use_role', 'adhoc_role')
|
||||
return qs
|
||||
|
||||
class InventoryDetail(RetrieveUpdateDestroyAPIView):
|
||||
@ -2188,9 +2189,6 @@ class JobTemplateDetail(RetrieveUpdateDestroyAPIView):
|
||||
can_delete = request.user.can_access(JobTemplate, 'delete', obj)
|
||||
if not can_delete:
|
||||
raise PermissionDenied("Cannot delete job template.")
|
||||
if obj.jobs.filter(status__in=['new', 'pending', 'waiting', 'running']).exists():
|
||||
return Response({"error": "Delete not allowed while there are jobs running"},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
return super(JobTemplateDetail, self).destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
@ -2591,7 +2589,7 @@ class SystemJobTemplateList(ListAPIView):
|
||||
serializer_class = SystemJobTemplateSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not request.user.is_superuser:
|
||||
if not request.user.is_superuser and not request.user.is_system_auditor:
|
||||
raise PermissionDenied("Superuser privileges needed.")
|
||||
return super(SystemJobTemplateList, self).get(request, *args, **kwargs)
|
||||
|
||||
@ -3321,7 +3319,7 @@ class SystemJobList(ListCreateAPIView):
|
||||
serializer_class = SystemJobListSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not request.user.is_superuser:
|
||||
if not request.user.is_superuser and not request.user.is_system_auditor:
|
||||
raise PermissionDenied("Superuser privileges needed.")
|
||||
return super(SystemJobList, self).get(request, *args, **kwargs)
|
||||
|
||||
@ -3625,8 +3623,6 @@ class RoleList(ListAPIView):
|
||||
new_in_300 = True
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_superuser:
|
||||
return Role.objects.all()
|
||||
return Role.visible_roles(self.request.user)
|
||||
|
||||
|
||||
@ -3652,10 +3648,10 @@ class RoleUsersList(SubListCreateAttachDetachAPIView):
|
||||
return role.members.all()
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# Forbid implicit role creation here
|
||||
# Forbid implicit user creation here
|
||||
sub_id = request.data.get('id', None)
|
||||
if not sub_id:
|
||||
data = dict(msg="Role 'id' field is missing.")
|
||||
data = dict(msg="User 'id' field is missing.")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
return super(RoleUsersList, self).post(request, *args, **kwargs)
|
||||
|
||||
|
||||
@ -12,11 +12,12 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.exceptions import ParseError, PermissionDenied
|
||||
from rest_framework.exceptions import ParseError, PermissionDenied, ValidationError
|
||||
|
||||
# AWX
|
||||
from awx.main.utils import * # noqa
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.models.unified_jobs import ACTIVE_STATES
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
from awx.api.license import LicenseForbids
|
||||
from awx.main.task_engine import TaskSerializer
|
||||
@ -139,7 +140,7 @@ class BaseAccess(object):
|
||||
self.user = user
|
||||
|
||||
def get_queryset(self):
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return self.model.objects.all()
|
||||
else:
|
||||
return self.model.objects.none()
|
||||
@ -221,7 +222,7 @@ class UserAccess(BaseAccess):
|
||||
model = User
|
||||
|
||||
def get_queryset(self):
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return User.objects.all()
|
||||
|
||||
if tower_settings.ORG_ADMINS_CAN_SEE_ALL_USERS and \
|
||||
@ -310,7 +311,16 @@ class OrganizationAccess(BaseAccess):
|
||||
|
||||
def can_delete(self, obj):
|
||||
self.check_license(feature='multiple_organizations', check_expiration=False)
|
||||
return self.can_change(obj, None)
|
||||
is_change_possible = self.can_change(obj, None)
|
||||
if not is_change_possible:
|
||||
return False
|
||||
active_jobs = []
|
||||
active_jobs.extend(Job.objects.filter(project__in=obj.projects.all(), status__in=ACTIVE_STATES))
|
||||
active_jobs.extend(ProjectUpdate.objects.filter(project__in=obj.projects.all(), status__in=ACTIVE_STATES))
|
||||
active_jobs.extend(InventoryUpdate.objects.filter(inventory_source__inventory__organization=obj, status__in=ACTIVE_STATES))
|
||||
if len(active_jobs) > 0:
|
||||
raise ValidationError("Delete not allowed while there are jobs running. Number of jobs {}".format(len(active_jobs)))
|
||||
return True
|
||||
|
||||
class InventoryAccess(BaseAccess):
|
||||
'''
|
||||
@ -373,7 +383,15 @@ class InventoryAccess(BaseAccess):
|
||||
return self.user in obj.admin_role
|
||||
|
||||
def can_delete(self, obj):
|
||||
return self.can_admin(obj, None)
|
||||
is_can_admin = self.can_admin(obj, None)
|
||||
if not is_can_admin:
|
||||
return False
|
||||
active_jobs = []
|
||||
active_jobs.extend(Job.objects.filter(inventory=obj, status__in=ACTIVE_STATES))
|
||||
active_jobs.extend(InventoryUpdate.objects.filter(inventory_source__inventory=obj, status__in=ACTIVE_STATES))
|
||||
if len(active_jobs) > 0:
|
||||
raise ValidationError("Delete not allowed while there are jobs running. Number of jobs {}".format(len(active_jobs)))
|
||||
return True
|
||||
|
||||
def can_run_ad_hoc_commands(self, obj):
|
||||
return self.user in obj.adhoc_role
|
||||
@ -486,7 +504,14 @@ class GroupAccess(BaseAccess):
|
||||
return True
|
||||
|
||||
def can_delete(self, obj):
|
||||
return obj and self.user in obj.inventory.admin_role
|
||||
is_delete_allowed = bool(obj and self.user in obj.inventory.admin_role)
|
||||
if not is_delete_allowed:
|
||||
return False
|
||||
active_jobs = []
|
||||
active_jobs.extend(InventoryUpdate.objects.filter(inventory_source__in=obj.inventory_sources.all(), status__in=ACTIVE_STATES))
|
||||
if len(active_jobs) > 0:
|
||||
raise ValidationError("Delete not allowed while there are jobs running. Number of jobs {}".format(len(active_jobs)))
|
||||
return True
|
||||
|
||||
class InventorySourceAccess(BaseAccess):
|
||||
'''
|
||||
@ -589,8 +614,9 @@ class CredentialAccess(BaseAccess):
|
||||
def can_read(self, obj):
|
||||
return self.user in obj.read_role
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
if self.user.is_superuser:
|
||||
if not data: # So the browseable API will work
|
||||
return True
|
||||
user_pk = get_pk_from_dict(data, 'user')
|
||||
if user_pk:
|
||||
@ -660,6 +686,8 @@ class TeamAccess(BaseAccess):
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
if not data: # So the browseable API will work
|
||||
return Organization.accessible_objects(self.user, 'admin_role').exists()
|
||||
org_pk = get_pk_from_dict(data, 'organization')
|
||||
org = get_object_or_400(Organization, pk=org_pk)
|
||||
if self.user in org.admin_role:
|
||||
@ -715,13 +743,15 @@ class ProjectAccess(BaseAccess):
|
||||
model = Project
|
||||
|
||||
def get_queryset(self):
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return self.model.objects.all()
|
||||
qs = self.model.accessible_objects(self.user, 'read_role')
|
||||
return qs.select_related('modified_by', 'credential', 'current_job', 'last_job').all()
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
if not data: # So the browseable API will work
|
||||
return Organization.accessible_objects(self.user, 'admin_role').exists()
|
||||
organization_pk = get_pk_from_dict(data, 'organization')
|
||||
org = get_object_or_400(Organization, pk=organization_pk)
|
||||
return self.user in org.admin_role
|
||||
@ -731,7 +761,15 @@ class ProjectAccess(BaseAccess):
|
||||
return self.user in obj.admin_role
|
||||
|
||||
def can_delete(self, obj):
|
||||
return self.can_change(obj, None)
|
||||
is_change_allowed = self.can_change(obj, None)
|
||||
if not is_change_allowed:
|
||||
return False
|
||||
active_jobs = []
|
||||
active_jobs.extend(Job.objects.filter(project=obj, status__in=ACTIVE_STATES))
|
||||
active_jobs.extend(ProjectUpdate.objects.filter(project=obj, status__in=ACTIVE_STATES))
|
||||
if len(active_jobs) > 0:
|
||||
raise ValidationError("Delete not allowed while there are jobs running. Number of jobs {}".format(len(active_jobs)))
|
||||
return True
|
||||
|
||||
@check_superuser
|
||||
def can_start(self, obj):
|
||||
@ -747,7 +785,7 @@ class ProjectUpdateAccess(BaseAccess):
|
||||
model = ProjectUpdate
|
||||
|
||||
def get_queryset(self):
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return self.model.objects.all()
|
||||
qs = ProjectUpdate.objects.distinct()
|
||||
qs = qs.select_related('created_by', 'modified_by', 'project')
|
||||
@ -783,7 +821,7 @@ class JobTemplateAccess(BaseAccess):
|
||||
model = JobTemplate
|
||||
|
||||
def get_queryset(self):
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
qs = self.model.objects.all()
|
||||
else:
|
||||
qs = self.model.accessible_objects(self.user, 'read_role')
|
||||
@ -802,7 +840,7 @@ class JobTemplateAccess(BaseAccess):
|
||||
given action as well as the 'create' deploy permission.
|
||||
Users who are able to create deploy jobs can also run normal and check (dry run) jobs.
|
||||
'''
|
||||
if not data or '_method' in data: # So the browseable API will work?
|
||||
if not data: # So the browseable API will work
|
||||
return True
|
||||
|
||||
# if reference_obj is provided, determine if it can be coppied
|
||||
@ -953,7 +991,13 @@ class JobTemplateAccess(BaseAccess):
|
||||
|
||||
@check_superuser
|
||||
def can_delete(self, obj):
|
||||
return self.user in obj.admin_role
|
||||
is_delete_allowed = self.user in obj.admin_role
|
||||
if not is_delete_allowed:
|
||||
return False
|
||||
active_jobs = obj.jobs.filter(status__in=ACTIVE_STATES)
|
||||
if len(active_jobs) > 0:
|
||||
raise ValidationError("Delete not allowed while there are jobs running. Number of jobs {}".format(len(active_jobs)))
|
||||
return True
|
||||
|
||||
class JobAccess(BaseAccess):
|
||||
'''
|
||||
@ -974,7 +1018,7 @@ class JobAccess(BaseAccess):
|
||||
qs = qs.select_related('created_by', 'modified_by', 'job_template', 'inventory',
|
||||
'project', 'credential', 'cloud_credential', 'job_template')
|
||||
qs = qs.prefetch_related('unified_job_template')
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return qs.all()
|
||||
|
||||
qs_jt = qs.filter(
|
||||
@ -992,7 +1036,7 @@ class JobAccess(BaseAccess):
|
||||
Q(project__organization__in=org_access_qs)).distinct()
|
||||
|
||||
def can_add(self, data):
|
||||
if not data or '_method' in data: # So the browseable API will work?
|
||||
if not data: # So the browseable API will work
|
||||
return True
|
||||
if not self.user.is_superuser:
|
||||
return False
|
||||
@ -1073,10 +1117,7 @@ class AdHocCommandAccess(BaseAccess):
|
||||
'''
|
||||
I can only see/run ad hoc commands when:
|
||||
- I am a superuser.
|
||||
- I am an org admin and have permission to read the credential.
|
||||
- I am a normal user with a user/team permission that has at least read
|
||||
permission on the inventory and the run_ad_hoc_commands flag set, and I
|
||||
can read the credential.
|
||||
- I have read access to the inventory
|
||||
'''
|
||||
model = AdHocCommand
|
||||
|
||||
@ -1084,26 +1125,23 @@ class AdHocCommandAccess(BaseAccess):
|
||||
qs = self.model.objects.distinct()
|
||||
qs = qs.select_related('created_by', 'modified_by', 'inventory',
|
||||
'credential')
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return qs.all()
|
||||
|
||||
credential_ids = set(self.user.get_queryset(Credential).values_list('id', flat=True))
|
||||
inventory_qs = Inventory.accessible_objects(self.user, 'read_role')
|
||||
|
||||
return qs.filter(credential_id__in=credential_ids,
|
||||
inventory__in=inventory_qs)
|
||||
return qs.filter(inventory__in=inventory_qs)
|
||||
|
||||
def can_add(self, data):
|
||||
if not data or '_method' in data: # So the browseable API will work?
|
||||
if not data: # So the browseable API will work
|
||||
return True
|
||||
|
||||
self.check_license()
|
||||
|
||||
# If a credential is provided, the user should have read access to it.
|
||||
# If a credential is provided, the user should have use access to it.
|
||||
credential_pk = get_pk_from_dict(data, 'credential')
|
||||
if credential_pk:
|
||||
credential = get_object_or_400(Credential, pk=credential_pk)
|
||||
if self.user not in credential.read_role:
|
||||
if self.user not in credential.use_role:
|
||||
return False
|
||||
|
||||
# Check that the user has the run ad hoc command permission on the
|
||||
@ -1148,7 +1186,7 @@ class AdHocCommandEventAccess(BaseAccess):
|
||||
qs = self.model.objects.distinct()
|
||||
qs = qs.select_related('ad_hoc_command', 'host')
|
||||
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return qs.all()
|
||||
ad_hoc_command_qs = self.user.get_queryset(AdHocCommand)
|
||||
host_qs = self.user.get_queryset(Host)
|
||||
@ -1174,7 +1212,7 @@ class JobHostSummaryAccess(BaseAccess):
|
||||
def get_queryset(self):
|
||||
qs = self.model.objects
|
||||
qs = qs.select_related('job', 'job__job_template', 'host')
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return qs.all()
|
||||
job_qs = self.user.get_queryset(Job)
|
||||
host_qs = self.user.get_queryset(Host)
|
||||
@ -1206,7 +1244,7 @@ class JobEventAccess(BaseAccess):
|
||||
event_data__icontains='"ansible_job_id": "',
|
||||
event_data__contains='"module_name": "async_status"')
|
||||
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return qs.all()
|
||||
|
||||
job_qs = self.user.get_queryset(Job)
|
||||
@ -1319,7 +1357,7 @@ class ScheduleAccess(BaseAccess):
|
||||
qs = self.model.objects.all()
|
||||
qs = qs.select_related('created_by', 'modified_by')
|
||||
qs = qs.prefetch_related('unified_job_template')
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return qs.all()
|
||||
job_template_qs = self.user.get_queryset(JobTemplate)
|
||||
inventory_source_qs = self.user.get_queryset(InventorySource)
|
||||
@ -1370,14 +1408,19 @@ class NotificationTemplateAccess(BaseAccess):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.model.objects.all()
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return qs
|
||||
return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, 'admin_role').all())
|
||||
return self.model.objects.filter(
|
||||
Q(organization__in=self.user.admin_of_organizations) |
|
||||
Q(organization__in=self.user.auditor_of_organizations)
|
||||
).distinct()
|
||||
|
||||
@check_superuser
|
||||
def can_read(self, obj):
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return True
|
||||
if obj.organization is not None:
|
||||
return self.user in obj.organization.admin_role
|
||||
if self.user in obj.organization.admin_role or self.user in obj.organization.auditor_role:
|
||||
return True
|
||||
return False
|
||||
|
||||
@check_superuser
|
||||
@ -1414,9 +1457,12 @@ class NotificationAccess(BaseAccess):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.model.objects.all()
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return qs
|
||||
return self.model.objects.filter(notification_template__organization__in=Organization.accessible_objects(self.user, 'admin_role'))
|
||||
return self.model.objects.filter(
|
||||
Q(notification_template__organization__in=self.user.admin_of_organizations) |
|
||||
Q(notification_template__organization__in=self.user.auditor_of_organizations)
|
||||
).distinct()
|
||||
|
||||
def can_read(self, obj):
|
||||
return self.user.can_access(NotificationTemplate, 'read', obj.notification_template)
|
||||
@ -1431,7 +1477,7 @@ class LabelAccess(BaseAccess):
|
||||
model = Label
|
||||
|
||||
def get_queryset(self):
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return self.model.objects.all()
|
||||
return self.model.objects.filter(
|
||||
organization__in=Organization.accessible_objects(self.user, 'read_role')
|
||||
@ -1443,7 +1489,7 @@ class LabelAccess(BaseAccess):
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
if not data or '_method' in data: # So the browseable API will work?
|
||||
if not data: # So the browseable API will work
|
||||
return True
|
||||
|
||||
org_pk = get_pk_from_dict(data, 'organization')
|
||||
@ -1494,9 +1540,7 @@ class ActivityStreamAccess(BaseAccess):
|
||||
'inventory_update', 'credential', 'team', 'project', 'project_update',
|
||||
'permission', 'job_template', 'job', 'ad_hoc_command',
|
||||
'notification_template', 'notification', 'label', 'role')
|
||||
if self.user.is_superuser:
|
||||
return qs.all()
|
||||
if self.user in Role.singleton('system_auditor'):
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return qs.all()
|
||||
|
||||
inventory_set = Inventory.accessible_objects(self.user, 'read_role')
|
||||
@ -1544,18 +1588,20 @@ class CustomInventoryScriptAccess(BaseAccess):
|
||||
model = CustomInventoryScript
|
||||
|
||||
def get_queryset(self):
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return self.model.objects.distinct().all()
|
||||
return self.model.accessible_objects(self.user, 'read_role').all()
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
if not data: # So the browseable API will work
|
||||
return Organization.accessible_objects(self.user, 'admin_role').exists()
|
||||
org_pk = get_pk_from_dict(data, 'organization')
|
||||
org = get_object_or_400(Organization, pk=org_pk)
|
||||
return self.user in org.admin_role
|
||||
|
||||
@check_superuser
|
||||
def can_admin(self, obj):
|
||||
def can_admin(self, obj, data=None):
|
||||
return self.user in obj.admin_role
|
||||
|
||||
@check_superuser
|
||||
@ -1566,10 +1612,6 @@ class CustomInventoryScriptAccess(BaseAccess):
|
||||
def can_delete(self, obj):
|
||||
return self.can_admin(obj)
|
||||
|
||||
@check_superuser
|
||||
def can_read(self, obj):
|
||||
return self.user in obj.read_role
|
||||
|
||||
|
||||
class TowerSettingsAccess(BaseAccess):
|
||||
'''
|
||||
@ -1598,7 +1640,7 @@ class RoleAccess(BaseAccess):
|
||||
def can_read(self, obj):
|
||||
if not obj:
|
||||
return False
|
||||
if self.user.is_superuser:
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return True
|
||||
|
||||
if obj.object_id:
|
||||
|
||||
@ -24,7 +24,7 @@ def create_system_job_templates(apps, schema_editor):
|
||||
job_type='cleanup_jobs',
|
||||
defaults=dict(
|
||||
name='Cleanup Job Details',
|
||||
description='Remove job history older than X days',
|
||||
description='Remove job history',
|
||||
created=now_dt,
|
||||
modified=now_dt,
|
||||
polymorphic_ctype=sjt_ct,
|
||||
@ -51,7 +51,7 @@ def create_system_job_templates(apps, schema_editor):
|
||||
job_type='cleanup_activitystream',
|
||||
defaults=dict(
|
||||
name='Cleanup Activity Stream',
|
||||
description='Remove activity stream history older than X days',
|
||||
description='Remove activity stream history',
|
||||
created=now_dt,
|
||||
modified=now_dt,
|
||||
polymorphic_ctype=sjt_ct,
|
||||
|
||||
20
awx/main/migrations/0027_v300_team_migrations.py
Normal file
20
awx/main/migrations/0027_v300_team_migrations.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from awx.main.migrations import _team_cleanup as team_cleanup
|
||||
from awx.main.migrations import _migration_utils as migration_utils
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0026_v300_credential_unique'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
||||
migrations.RunPython(team_cleanup.migrate_team),
|
||||
migrations.RunPython(rbac.rebuild_role_hierarchy),
|
||||
]
|
||||
21
awx/main/migrations/0028_v300_org_team_cascade.py
Normal file
21
awx/main/migrations/0028_v300_org_team_cascade.py
Normal file
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import awx.main.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0027_v300_team_migrations'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='organization',
|
||||
field=models.ForeignKey(related_name='teams', to='main.Organization'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -656,7 +656,7 @@ class TeamAccess(BaseAccess):
|
||||
raise PermissionDenied('Unable to change organization on a team')
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
if self.user in obj.organization.deprecated_admins.all():
|
||||
if obj.organization and self.user in obj.organization.deprecated_admins.all():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@ -198,7 +198,8 @@ def migrate_credential(apps, schema_editor):
|
||||
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at organization level".format(cred.name, cred.kind, cred.host)))
|
||||
|
||||
if cred.deprecated_team is not None:
|
||||
cred.deprecated_team.member_role.children.add(cred.admin_role)
|
||||
cred.deprecated_team.admin_role.children.add(cred.admin_role)
|
||||
cred.deprecated_team.member_role.children.add(cred.use_role)
|
||||
cred.save()
|
||||
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host)))
|
||||
elif cred.deprecated_user is not None:
|
||||
|
||||
30
awx/main/migrations/_team_cleanup.py
Normal file
30
awx/main/migrations/_team_cleanup.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Python
|
||||
import logging
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def log_migration(wrapped):
|
||||
'''setup the logging mechanism for each migration method
|
||||
as it runs, Django resets this, so we use a decorator
|
||||
to re-add the handler for each method.
|
||||
'''
|
||||
handler = logging.FileHandler("/tmp/tower_rbac_migrations.log", mode="a", encoding="UTF-8")
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
handler.setLevel(logging.DEBUG)
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
logger.handlers = []
|
||||
logger.addHandler(handler)
|
||||
return wrapped(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@log_migration
|
||||
def migrate_team(apps, schema_editor):
|
||||
'''If an orphan team exists that is still active, delete it.'''
|
||||
Team = apps.get_model('main', 'Team')
|
||||
for team in Team.objects.iterator():
|
||||
if team.organization is None:
|
||||
logger.info(smart_text(u"Deleting orphaned team: {}".format(team.name)))
|
||||
team.delete()
|
||||
@ -309,8 +309,7 @@ class Inventory(CommonModel, ResourceMixin):
|
||||
else:
|
||||
computed_fields.pop(field)
|
||||
if computed_fields:
|
||||
if len(computed_fields) > 0:
|
||||
iobj.save(update_fields=computed_fields.keys())
|
||||
iobj.save(update_fields=computed_fields.keys())
|
||||
logger.debug("Finished updating inventory computed fields")
|
||||
|
||||
@property
|
||||
|
||||
@ -681,10 +681,10 @@ class Job(UnifiedJob, JobOptions):
|
||||
ok=h.ok,
|
||||
processed=h.processed,
|
||||
skipped=h.skipped)
|
||||
data.update(dict(inventory=self.inventory.name,
|
||||
data.update(dict(inventory=self.inventory.name if self.inventory else None,
|
||||
project=self.project.name if self.project else None,
|
||||
playbook=self.playbook,
|
||||
credential=self.credential.name,
|
||||
credential=self.credential.name if self.credential else None,
|
||||
limit=self.limit,
|
||||
extra_vars=self.extra_vars,
|
||||
hosts=all_hosts))
|
||||
|
||||
@ -92,8 +92,8 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
|
||||
organization = models.ForeignKey(
|
||||
'Organization',
|
||||
blank=False,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
null=False,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='teams',
|
||||
)
|
||||
deprecated_projects = models.ManyToManyField(
|
||||
|
||||
@ -39,6 +39,7 @@ __all__ = ['UnifiedJobTemplate', 'UnifiedJob']
|
||||
logger = logging.getLogger('awx.main.models.unified_jobs')
|
||||
|
||||
CAN_CANCEL = ('new', 'pending', 'waiting', 'running')
|
||||
ACTIVE_STATES = CAN_CANCEL
|
||||
|
||||
|
||||
class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, NotificationFieldsModel):
|
||||
|
||||
@ -323,6 +323,7 @@ model_serializer_mapping = {
|
||||
Host: HostSerializer,
|
||||
Group: GroupSerializer,
|
||||
InventorySource: InventorySourceSerializer,
|
||||
CustomInventoryScript: CustomInventoryScriptSerializer,
|
||||
Credential: CredentialSerializer,
|
||||
Team: TeamSerializer,
|
||||
Project: ProjectSerializer,
|
||||
@ -410,7 +411,10 @@ def activity_stream_associate(sender, instance, **kwargs):
|
||||
for entity_acted in kwargs['pk_set']:
|
||||
obj2 = kwargs['model']
|
||||
obj2_id = entity_acted
|
||||
obj2_actual = obj2.objects.get(id=obj2_id)
|
||||
obj2_actual = obj2.objects.filter(id=obj2_id)
|
||||
if not obj2_actual.exists():
|
||||
continue
|
||||
obj2_actual = obj2_actual[0]
|
||||
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__)
|
||||
|
||||
@ -599,7 +599,8 @@ class BaseTask(Task):
|
||||
else:
|
||||
child_procs = main_proc.get_children(recursive=True)
|
||||
for child_proc in child_procs:
|
||||
os.kill(child_proc.pid, signal.SIGTERM)
|
||||
os.kill(child_proc.pid, signal.SIGKILL)
|
||||
os.kill(main_proc.pid, signal.SIGKILL)
|
||||
except TypeError:
|
||||
os.kill(child.pid, signal.SIGKILL)
|
||||
else:
|
||||
|
||||
43
awx/main/tests/URI.py
Normal file
43
awx/main/tests/URI.py
Normal file
@ -0,0 +1,43 @@
|
||||
# Helps with test cases.
|
||||
# Save all components of a uri (i.e. scheme, username, password, etc.) so that
|
||||
# when we construct a uri string and decompose it, we can verify the decomposition
|
||||
class URI(object):
|
||||
DEFAULTS = {
|
||||
'scheme' : 'http',
|
||||
'username' : 'MYUSERNAME',
|
||||
'password' : 'MYPASSWORD',
|
||||
'host' : 'host.com',
|
||||
}
|
||||
|
||||
def __init__(self, description='N/A', scheme=DEFAULTS['scheme'], username=DEFAULTS['username'], password=DEFAULTS['password'], host=DEFAULTS['host']):
|
||||
self.description = description
|
||||
self.scheme = scheme
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.host = host
|
||||
|
||||
def get_uri(self):
|
||||
uri = "%s://" % self.scheme
|
||||
if self.username:
|
||||
uri += "%s" % self.username
|
||||
if self.password:
|
||||
uri += ":%s" % self.password
|
||||
if (self.username or self.password) and self.host is not None:
|
||||
uri += "@%s" % self.host
|
||||
elif self.host is not None:
|
||||
uri += "%s" % self.host
|
||||
return uri
|
||||
|
||||
def get_secret_count(self):
|
||||
secret_count = 0
|
||||
if self.username:
|
||||
secret_count += 1
|
||||
if self.password:
|
||||
secret_count += 1
|
||||
return secret_count
|
||||
|
||||
def __string__(self):
|
||||
return self.get_uri()
|
||||
|
||||
def __repr__(self):
|
||||
return self.get_uri()
|
||||
@ -35,6 +35,7 @@ from awx.main.management.commands.run_task_system import run_taskmanager
|
||||
from awx.main.utils import get_ansible_version
|
||||
from awx.main.task_engine import TaskEngager as LicenseWriter
|
||||
from awx.sso.backends import LDAPSettings
|
||||
from awx.main.tests.URI import URI # noqa
|
||||
|
||||
TEST_PLAYBOOK = '''- hosts: mygroup
|
||||
gather_facts: false
|
||||
@ -732,48 +733,3 @@ class BaseJobExecutionTest(QueueStartStopTestMixin, BaseLiveServerTest):
|
||||
'''
|
||||
Base class for celery task tests.
|
||||
'''
|
||||
|
||||
# Helps with test cases.
|
||||
# Save all components of a uri (i.e. scheme, username, password, etc.) so that
|
||||
# when we construct a uri string and decompose it, we can verify the decomposition
|
||||
class URI(object):
|
||||
DEFAULTS = {
|
||||
'scheme' : 'http',
|
||||
'username' : 'MYUSERNAME',
|
||||
'password' : 'MYPASSWORD',
|
||||
'host' : 'host.com',
|
||||
}
|
||||
|
||||
def __init__(self, description='N/A', scheme=DEFAULTS['scheme'], username=DEFAULTS['username'], password=DEFAULTS['password'], host=DEFAULTS['host']):
|
||||
self.description = description
|
||||
self.scheme = scheme
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.host = host
|
||||
|
||||
def get_uri(self):
|
||||
uri = "%s://" % self.scheme
|
||||
if self.username:
|
||||
uri += "%s" % self.username
|
||||
if self.password:
|
||||
uri += ":%s" % self.password
|
||||
if (self.username or self.password) and self.host is not None:
|
||||
uri += "@%s" % self.host
|
||||
elif self.host is not None:
|
||||
uri += "%s" % self.host
|
||||
return uri
|
||||
|
||||
def get_secret_count(self):
|
||||
secret_count = 0
|
||||
if self.username:
|
||||
secret_count += 1
|
||||
if self.password:
|
||||
secret_count += 1
|
||||
return secret_count
|
||||
|
||||
def __string__(self):
|
||||
return self.get_uri()
|
||||
|
||||
def __repr__(self):
|
||||
return self.get_uri()
|
||||
|
||||
|
||||
@ -122,7 +122,7 @@ def test_get_inventory_ad_hoc_command_list(admin, alice, post_adhoc, get, invent
|
||||
|
||||
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
|
||||
assert res.data['count'] == 1
|
||||
|
||||
machine_credential.use_role.members.add(alice)
|
||||
res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv1.id,)), alice, expect=200)
|
||||
|
||||
190
awx/main/tests/functional/api/test_organizations.py
Normal file
190
awx/main/tests/functional/api/test_organizations.py
Normal file
@ -0,0 +1,190 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import pytest
|
||||
import mock
|
||||
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_list_access_tests(options, head, get, admin, alice):
|
||||
options(reverse('api:organization_list'), user=admin, expect=200)
|
||||
head(reverse('api:organization_list'), user=admin, expect=200)
|
||||
get(reverse('api:organization_list'), user=admin, expect=200)
|
||||
options(reverse('api:organization_list'), user=alice, expect=200)
|
||||
head(reverse('api:organization_list'), user=alice, expect=200)
|
||||
get(reverse('api:organization_list'), user=alice, expect=200)
|
||||
options(reverse('api:organization_list'), user=None, expect=401)
|
||||
head(reverse('api:organization_list'), user=None, expect=401)
|
||||
get(reverse('api:organization_list'), user=None, expect=401)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_access_tests(organization, get, admin, alice, bob):
|
||||
organization.member_role.members.add(alice)
|
||||
get(reverse('api:organization_detail', args=(organization.id,)), user=admin, expect=200)
|
||||
get(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=200)
|
||||
get(reverse('api:organization_detail', args=(organization.id,)), user=bob, expect=403)
|
||||
get(reverse('api:organization_detail', args=(organization.id,)), user=None, expect=401)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_list_integrity(organization, get, admin, alice):
|
||||
res = get(reverse('api:organization_list'), user=admin)
|
||||
for field in ['id', 'url', 'name', 'description', 'created']:
|
||||
assert field in res.data['results'][0]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_list_visibility(organizations, get, admin, alice):
|
||||
orgs = organizations(2)
|
||||
|
||||
res = get(reverse('api:organization_list'), user=admin)
|
||||
assert res.data['count'] == 2
|
||||
assert len(res.data['results']) == 2
|
||||
|
||||
res = get(reverse('api:organization_list'), user=alice)
|
||||
assert res.data['count'] == 0
|
||||
|
||||
orgs[1].member_role.members.add(alice)
|
||||
|
||||
res = get(reverse('api:organization_list'), user=alice)
|
||||
assert res.data['count'] == 1
|
||||
assert len(res.data['results']) == 1
|
||||
assert res.data['results'][0]['id'] == orgs[1].id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_project_list(organization, project_factory, get, alice, bob, rando):
|
||||
prj1 = project_factory('project-one')
|
||||
project_factory('project-two')
|
||||
organization.admin_role.members.add(alice)
|
||||
organization.member_role.members.add(bob)
|
||||
prj1.use_role.members.add(bob)
|
||||
assert get(reverse('api:organization_projects_list', args=(organization.id,)), user=alice).data['count'] == 2
|
||||
assert get(reverse('api:organization_projects_list', args=(organization.id,)), user=bob).data['count'] == 1
|
||||
assert get(reverse('api:organization_projects_list', args=(organization.id,)), user=rando).status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_user_list(organization, get, admin, alice, bob):
|
||||
organization.admin_role.members.add(alice)
|
||||
organization.member_role.members.add(alice)
|
||||
organization.member_role.members.add(bob)
|
||||
assert get(reverse('api:organization_users_list', args=(organization.id,)), user=admin).data['count'] == 2
|
||||
assert get(reverse('api:organization_users_list', args=(organization.id,)), user=alice).data['count'] == 2
|
||||
assert get(reverse('api:organization_users_list', args=(organization.id,)), user=bob).data['count'] == 2
|
||||
assert get(reverse('api:organization_admins_list', args=(organization.id,)), user=admin).data['count'] == 1
|
||||
assert get(reverse('api:organization_admins_list', args=(organization.id,)), user=alice).data['count'] == 1
|
||||
assert get(reverse('api:organization_admins_list', args=(organization.id,)), user=bob).data['count'] == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_inventory_list(organization, inventory_factory, get, alice, bob, rando):
|
||||
inv1 = inventory_factory('inventory-one')
|
||||
inventory_factory('inventory-two')
|
||||
organization.admin_role.members.add(alice)
|
||||
organization.member_role.members.add(bob)
|
||||
inv1.use_role.members.add(bob)
|
||||
assert get(reverse('api:organization_inventories_list', args=(organization.id,)), user=alice).data['count'] == 2
|
||||
assert get(reverse('api:organization_inventories_list', args=(organization.id,)), user=bob).data['count'] == 1
|
||||
get(reverse('api:organization_inventories_list', args=(organization.id,)), user=rando, expect=403)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True)
|
||||
def test_create_organization(post, admin, alice):
|
||||
new_org = {
|
||||
'name': 'new org',
|
||||
'description': 'my description'
|
||||
}
|
||||
res = post(reverse('api:organization_list'), new_org, user=admin, expect=201)
|
||||
assert res.data['name'] == new_org['name']
|
||||
res = post(reverse('api:organization_list'), new_org, user=admin, expect=400)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True)
|
||||
def test_create_organization_xfail(post, alice):
|
||||
new_org = {
|
||||
'name': 'new org',
|
||||
'description': 'my description'
|
||||
}
|
||||
post(reverse('api:organization_list'), new_org, user=alice, expect=403)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_add_user_to_organization(post, organization, alice, bob):
|
||||
organization.admin_role.members.add(alice)
|
||||
post(reverse('api:organization_users_list', args=(organization.id,)), {'id': bob.id}, user=alice, expect=204)
|
||||
assert bob in organization.member_role
|
||||
post(reverse('api:organization_users_list', args=(organization.id,)), {'id': bob.id, 'disassociate': True} , user=alice, expect=204)
|
||||
assert bob not in organization.member_role
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_add_user_to_organization_xfail(post, organization, alice, bob):
|
||||
organization.member_role.members.add(alice)
|
||||
post(reverse('api:organization_users_list', args=(organization.id,)), {'id': bob.id}, user=alice, expect=403)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_add_admin_to_organization(post, organization, alice, bob):
|
||||
organization.admin_role.members.add(alice)
|
||||
post(reverse('api:organization_admins_list', args=(organization.id,)), {'id': bob.id}, user=alice, expect=204)
|
||||
assert bob in organization.admin_role
|
||||
assert bob in organization.member_role
|
||||
post(reverse('api:organization_admins_list', args=(organization.id,)), {'id': bob.id, 'disassociate': True} , user=alice, expect=204)
|
||||
assert bob not in organization.admin_role
|
||||
assert bob not in organization.member_role
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_add_admin_to_organization_xfail(post, organization, alice, bob):
|
||||
organization.member_role.members.add(alice)
|
||||
post(reverse('api:organization_admins_list', args=(organization.id,)), {'id': bob.id}, user=alice, expect=403)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_organization(get, put, organization, alice, bob):
|
||||
organization.admin_role.members.add(alice)
|
||||
data = get(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=200).data
|
||||
data['description'] = 'hi'
|
||||
put(reverse('api:organization_detail', args=(organization.id,)), data, user=alice, expect=200)
|
||||
organization.refresh_from_db()
|
||||
assert organization.description == 'hi'
|
||||
data['description'] = 'bye'
|
||||
put(reverse('api:organization_detail', args=(organization.id,)), data, user=bob, expect=403)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
|
||||
def test_delete_organization(delete, organization, admin):
|
||||
delete(reverse('api:organization_detail', args=(organization.id,)), user=admin, expect=204)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
|
||||
def test_delete_organization2(delete, organization, alice):
|
||||
organization.admin_role.members.add(alice)
|
||||
delete(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=204)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
|
||||
def test_delete_organization_xfail1(delete, organization, alice):
|
||||
organization.member_role.members.add(alice)
|
||||
delete(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=403)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
|
||||
def test_delete_organization_xfail2(delete, organization):
|
||||
delete(reverse('api:organization_detail', args=(organization.id,)), user=None, expect=401)
|
||||
@ -1,8 +1,83 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.models import UnifiedJob
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from awx.main.models import UnifiedJob, ProjectUpdate
|
||||
from awx.main.tests.base import URI
|
||||
|
||||
|
||||
TEST_STDOUTS = []
|
||||
uri = URI(scheme="https", username="Dhh3U47nmC26xk9PKscV", password="PXPfWW8YzYrgS@E5NbQ2H@", host="github.ginger.com/theirrepo.git/info/refs")
|
||||
TEST_STDOUTS.append({
|
||||
'description': 'uri in a plain text document',
|
||||
'uri' : uri,
|
||||
'text' : 'hello world %s goodbye world' % uri,
|
||||
'occurrences' : 1
|
||||
})
|
||||
|
||||
uri = URI(scheme="https", username="applepie@@@", password="thatyouknow@@@@", host="github.ginger.com/theirrepo.git/info/refs")
|
||||
TEST_STDOUTS.append({
|
||||
'description': 'uri appears twice in a multiline plain text document',
|
||||
'uri' : uri,
|
||||
'text' : 'hello world %s \n\nyoyo\n\nhello\n%s' % (uri, uri),
|
||||
'occurrences' : 2
|
||||
})
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_cases(project):
|
||||
ret = []
|
||||
for e in TEST_STDOUTS:
|
||||
e['project'] = ProjectUpdate(project=project)
|
||||
e['project'].result_stdout_text = e['text']
|
||||
e['project'].save()
|
||||
ret.append(e)
|
||||
return ret
|
||||
|
||||
@pytest.fixture
|
||||
def negative_test_cases(job_factory):
|
||||
ret = []
|
||||
for e in TEST_STDOUTS:
|
||||
e['job'] = job_factory()
|
||||
e['job'].result_stdout_text = e['text']
|
||||
e['job'].save()
|
||||
ret.append(e)
|
||||
return ret
|
||||
|
||||
|
||||
formats = [
|
||||
('json', 'application/json'),
|
||||
('ansi', 'text/plain'),
|
||||
('txt', 'text/plain'),
|
||||
('html', 'text/html'),
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("format,content_type", formats)
|
||||
@pytest.mark.django_db
|
||||
def test_project_update_redaction_enabled(get, format, content_type, test_cases, admin):
|
||||
for test_data in test_cases:
|
||||
job = test_data['project']
|
||||
response = get(reverse("api:project_update_stdout", args=(job.pk,)) + "?format=" + format, user=admin, expect=200, accept=content_type)
|
||||
assert content_type in response['CONTENT-TYPE']
|
||||
assert response.data is not None
|
||||
content = response.data['content'] if format == 'json' else response.data
|
||||
assert test_data['uri'].username not in content
|
||||
assert test_data['uri'].password not in content
|
||||
assert content.count(test_data['uri'].host) == test_data['occurrences']
|
||||
|
||||
@pytest.mark.parametrize("format,content_type", formats)
|
||||
@pytest.mark.django_db
|
||||
def test_job_redaction_disabled(get, format, content_type, negative_test_cases, admin):
|
||||
for test_data in negative_test_cases:
|
||||
job = test_data['job']
|
||||
response = get(reverse("api:job_stdout", args=(job.pk,)) + "?format=" + format, user=admin, expect=200, format=format)
|
||||
content = response.data['content'] if format == 'json' else response.data
|
||||
assert response.data is not None
|
||||
assert test_data['uri'].username in content
|
||||
assert test_data['uri'].password in content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_options_fields_choices(instance, options, user):
|
||||
|
||||
@ -15,3 +90,4 @@ def test_options_fields_choices(instance, options, user):
|
||||
assert 'choice' == response.data['actions']['GET']['status']['type']
|
||||
assert UnifiedJob.STATUS_CHOICES == response.data['actions']['GET']['status']['choices']
|
||||
|
||||
|
||||
|
||||
@ -38,7 +38,10 @@ from awx.main.models.organization import (
|
||||
Team,
|
||||
)
|
||||
|
||||
from awx.main.models.notifications import NotificationTemplate
|
||||
from awx.main.models.notifications import (
|
||||
NotificationTemplate,
|
||||
Notification
|
||||
)
|
||||
|
||||
'''
|
||||
Disable all django model signals.
|
||||
@ -124,6 +127,12 @@ def project_factory(organization):
|
||||
return prj
|
||||
return factory
|
||||
|
||||
@pytest.fixture
|
||||
def job_factory(job_template, admin):
|
||||
def factory(job_template=job_template, initial_state='new', created_by=admin):
|
||||
return job_template.create_job(created_by=created_by, status=initial_state)
|
||||
return factory
|
||||
|
||||
@pytest.fixture
|
||||
def team_factory(organization):
|
||||
def factory(name):
|
||||
@ -187,6 +196,15 @@ def notification_template(organization):
|
||||
notification_configuration=dict(url="http://localhost",
|
||||
headers={"Test": "Header"}))
|
||||
|
||||
@pytest.fixture
|
||||
def notification(notification_template):
|
||||
return Notification.objects.create(notification_template=notification_template,
|
||||
status='successful',
|
||||
notifications_sent=1,
|
||||
notification_type='email',
|
||||
recipients='admin@redhat.com',
|
||||
subject='email subject')
|
||||
|
||||
@pytest.fixture
|
||||
def job_with_secret_key(job_with_secret_key_factory):
|
||||
return job_with_secret_key_factory(persisted=True)
|
||||
@ -292,17 +310,23 @@ def permissions():
|
||||
'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':True,},
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def post():
|
||||
def rf(url, data, user=None, middleware=None, expect=None, **kwargs):
|
||||
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
||||
|
||||
def _request(verb):
|
||||
def rf(url, data_or_user=None, user=None, middleware=None, expect=None, **kwargs):
|
||||
if type(data_or_user) is User and user is None:
|
||||
user = data_or_user
|
||||
elif 'data' not in kwargs:
|
||||
kwargs['data'] = data_or_user
|
||||
if 'format' not in kwargs:
|
||||
kwargs['format'] = 'json'
|
||||
request = APIRequestFactory().post(url, data, **kwargs)
|
||||
|
||||
view, view_args, view_kwargs = resolve(urlparse(url)[2])
|
||||
request = getattr(APIRequestFactory(), verb)(url, **kwargs)
|
||||
if middleware:
|
||||
middleware.process_request(request)
|
||||
if user:
|
||||
force_authenticate(request, user=user)
|
||||
|
||||
response = view(request, *view_args, **view_kwargs)
|
||||
if middleware:
|
||||
middleware.process_response(request, response)
|
||||
@ -310,134 +334,37 @@ def post():
|
||||
if response.status_code != expect:
|
||||
print(response.data)
|
||||
assert response.status_code == expect
|
||||
response.render()
|
||||
return response
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
def post():
|
||||
return _request('post')
|
||||
|
||||
@pytest.fixture
|
||||
def get():
|
||||
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'
|
||||
request = APIRequestFactory().get(url, **kwargs)
|
||||
if middleware:
|
||||
middleware.process_request(request)
|
||||
if user:
|
||||
force_authenticate(request, user=user)
|
||||
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
|
||||
return _request('get')
|
||||
|
||||
@pytest.fixture
|
||||
def put():
|
||||
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'
|
||||
request = APIRequestFactory().put(url, data, **kwargs)
|
||||
if middleware:
|
||||
middleware.process_request(request)
|
||||
if user:
|
||||
force_authenticate(request, user=user)
|
||||
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
|
||||
return _request('put')
|
||||
|
||||
@pytest.fixture
|
||||
def patch():
|
||||
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'
|
||||
request = APIRequestFactory().patch(url, data, **kwargs)
|
||||
if middleware:
|
||||
middleware.process_request(request)
|
||||
if user:
|
||||
force_authenticate(request, user=user)
|
||||
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
|
||||
return _request('patch')
|
||||
|
||||
@pytest.fixture
|
||||
def delete():
|
||||
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'
|
||||
request = APIRequestFactory().delete(url, **kwargs)
|
||||
if middleware:
|
||||
middleware.process_request(request)
|
||||
if user:
|
||||
force_authenticate(request, user=user)
|
||||
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
|
||||
return _request('delete')
|
||||
|
||||
@pytest.fixture
|
||||
def head():
|
||||
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'
|
||||
request = APIRequestFactory().head(url, **kwargs)
|
||||
if middleware:
|
||||
middleware.process_request(request)
|
||||
if user:
|
||||
force_authenticate(request, user=user)
|
||||
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
|
||||
return _request('head')
|
||||
|
||||
@pytest.fixture
|
||||
def options():
|
||||
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'
|
||||
request = APIRequestFactory().options(url, data, **kwargs)
|
||||
if middleware:
|
||||
middleware.process_request(request)
|
||||
if user:
|
||||
force_authenticate(request, user=user)
|
||||
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
|
||||
return _request('options')
|
||||
|
||||
|
||||
|
||||
|
||||
132
awx/main/tests/functional/core/test_licenses.py
Normal file
132
awx/main/tests/functional/core/test_licenses.py
Normal file
@ -0,0 +1,132 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import json
|
||||
import mock
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
|
||||
from awx.main.models import Host
|
||||
from awx.main.task_engine import TaskSerializer, TaskEngager
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_license_writer(inventory, admin):
|
||||
writer = TaskEngager(
|
||||
company_name='acmecorp',
|
||||
contact_name='Michael DeHaan',
|
||||
contact_email='michael@ansibleworks.com',
|
||||
license_date=25000, # seconds since epoch
|
||||
instance_count=500)
|
||||
|
||||
data = writer.get_data()
|
||||
|
||||
Host.objects.bulk_create(
|
||||
[
|
||||
Host(
|
||||
name='host.%d' % n,
|
||||
inventory=inventory,
|
||||
created_by=admin,
|
||||
modified=datetime.now(),
|
||||
created=datetime.now())
|
||||
for n in range(12)
|
||||
]
|
||||
)
|
||||
|
||||
assert data['instance_count'] == 500
|
||||
assert data['contact_name'] == 'Michael DeHaan'
|
||||
assert data['contact_email'] == 'michael@ansibleworks.com'
|
||||
assert data['license_date'] == 25000
|
||||
assert data['license_key'] == "11bae31f31c6a6cdcb483a278cdbe98bd8ac5761acd7163a50090b0f098b3a13"
|
||||
|
||||
strdata = writer.get_string()
|
||||
strdata_loaded = json.loads(strdata)
|
||||
assert strdata_loaded == data
|
||||
|
||||
reader = TaskSerializer()
|
||||
|
||||
vdata = reader.from_string(strdata)
|
||||
|
||||
assert vdata['available_instances'] == 500
|
||||
assert vdata['current_instances'] == 12
|
||||
assert vdata['free_instances'] == 488
|
||||
assert vdata['date_warning'] is True
|
||||
assert vdata['date_expired'] is True
|
||||
assert vdata['license_date'] == 25000
|
||||
assert vdata['time_remaining'] < 0
|
||||
assert vdata['valid_key'] is True
|
||||
assert vdata['compliant'] is False
|
||||
assert vdata['subscription_name']
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_expired_licenses():
|
||||
reader = TaskSerializer()
|
||||
writer = TaskEngager(
|
||||
company_name='Tower',
|
||||
contact_name='Tower Admin',
|
||||
contact_email='tower@ansible.com',
|
||||
license_date=int(time.time() - 3600),
|
||||
instance_count=100,
|
||||
trial=True)
|
||||
strdata = writer.get_string()
|
||||
vdata = reader.from_string(strdata)
|
||||
|
||||
assert vdata['compliant'] is False
|
||||
assert vdata['grace_period_remaining'] < 0
|
||||
|
||||
writer = TaskEngager(
|
||||
company_name='Tower',
|
||||
contact_name='Tower Admin',
|
||||
contact_email='tower@ansible.com',
|
||||
license_date=int(time.time() - 2592001),
|
||||
instance_count=100,
|
||||
trial=False)
|
||||
strdata = writer.get_string()
|
||||
vdata = reader.from_string(strdata)
|
||||
|
||||
assert vdata['compliant'] is False
|
||||
assert vdata['grace_period_remaining'] < 0
|
||||
|
||||
writer = TaskEngager(
|
||||
company_name='Tower',
|
||||
contact_name='Tower Admin',
|
||||
contact_email='tower@ansible.com',
|
||||
license_date=int(time.time() - 3600),
|
||||
instance_count=100,
|
||||
trial=False)
|
||||
strdata = writer.get_string()
|
||||
vdata = reader.from_string(strdata)
|
||||
|
||||
assert vdata['compliant'] is False
|
||||
assert vdata['grace_period_remaining'] > 0
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_aws_license():
|
||||
os.environ['AWX_LICENSE_FILE'] = 'non-existent-license-file.json'
|
||||
|
||||
h, path = tempfile.mkstemp()
|
||||
with os.fdopen(h, 'w') as f:
|
||||
json.dump({'instance_count': 100}, f)
|
||||
|
||||
def fetch_ami(_self):
|
||||
_self.attributes['ami-id'] = 'ami-00000000'
|
||||
return True
|
||||
|
||||
def fetch_instance(_self):
|
||||
_self.attributes['instance-id'] = 'i-00000000'
|
||||
return True
|
||||
|
||||
with mock.patch('awx.main.task_engine.TEMPORARY_TASK_FILE', path):
|
||||
with mock.patch('awx.main.task_engine.TemporaryTaskEngine.fetch_ami', fetch_ami):
|
||||
with mock.patch('awx.main.task_engine.TemporaryTaskEngine.fetch_instance', fetch_instance):
|
||||
reader = TaskSerializer()
|
||||
license = reader.from_file()
|
||||
assert license['is_aws']
|
||||
assert license['time_remaining']
|
||||
assert license['free_instances'] > 0
|
||||
assert license['grace_period_remaining'] > 0
|
||||
|
||||
os.unlink(path)
|
||||
39
awx/main/tests/functional/test_auth_token_limit.py
Normal file
39
awx/main/tests/functional/test_auth_token_limit.py
Normal file
@ -0,0 +1,39 @@
|
||||
import pytest
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils.timezone import now as tz_now
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from awx.main.models import AuthToken, User
|
||||
|
||||
|
||||
@override_settings(AUTH_TOKEN_PER_USER=3)
|
||||
@pytest.mark.django_db
|
||||
def test_get_tokens_over_limit():
|
||||
now = tz_now()
|
||||
# Times are relative to now
|
||||
# (key, created on in seconds , expiration in seconds)
|
||||
test_data = [
|
||||
# a is implicitly expired
|
||||
("a", -1000, -10),
|
||||
# b's are invalid due to session limit of 3
|
||||
("b", -100, 60),
|
||||
("bb", -100, 60),
|
||||
("c", -90, 70),
|
||||
("d", -80, 80),
|
||||
("e", -70, 90),
|
||||
]
|
||||
user = User.objects.create_superuser('admin', 'foo@bar.com', 'password')
|
||||
for key, t_create, t_expire in test_data:
|
||||
AuthToken.objects.create(
|
||||
user=user,
|
||||
key=key,
|
||||
request_hash='this_is_a_hash',
|
||||
created=now + timedelta(seconds=t_create),
|
||||
expires=now + timedelta(seconds=t_expire),
|
||||
)
|
||||
invalid_tokens = AuthToken.get_tokens_over_limit(user, now=now)
|
||||
invalid_keys = [x.key for x in invalid_tokens]
|
||||
assert len(invalid_keys) == 2
|
||||
assert 'b' in invalid_keys
|
||||
assert 'bb' in invalid_keys
|
||||
@ -114,3 +114,19 @@ def test_create_project(post, organization, org_admin, org_member, admin, rando,
|
||||
assert result.status_code == expected_status_code
|
||||
if expected_status_code == 201:
|
||||
assert Project.objects.filter(name='Project', organization=organization).exists()
|
||||
|
||||
@pytest.mark.django_db()
|
||||
def test_create_project_null_organization(post, organization, admin):
|
||||
post(reverse('api:project_list'), { 'name': 't', 'organization': None}, admin, expect=201)
|
||||
|
||||
@pytest.mark.django_db()
|
||||
def test_create_project_null_organization_xfail(post, organization, org_admin):
|
||||
post(reverse('api:project_list'), { 'name': 't', 'organization': None}, org_admin, expect=400)
|
||||
|
||||
@pytest.mark.django_db()
|
||||
def test_patch_project_null_organization(patch, organization, project, admin):
|
||||
patch(reverse('api:project_detail', args=(project.id,)), { 'name': 't', 'organization': organization.id}, admin, expect=200)
|
||||
|
||||
@pytest.mark.django_db()
|
||||
def test_patch_project_null_organization_xfail(patch, project, org_admin):
|
||||
patch(reverse('api:project_detail', args=(project.id,)), { 'name': 't', 'organization': None}, org_admin, expect=400)
|
||||
|
||||
@ -28,8 +28,10 @@ def test_two_teams_same_cred_name(organization_factory):
|
||||
|
||||
rbac.migrate_credential(apps, None)
|
||||
|
||||
assert objects.teams.team1.member_role in cred1.admin_role.parents.all()
|
||||
assert objects.teams.team2.member_role in cred2.admin_role.parents.all()
|
||||
assert objects.teams.team1.admin_role in cred1.admin_role.parents.all()
|
||||
assert objects.teams.team2.admin_role in cred2.admin_role.parents.all()
|
||||
assert objects.teams.team1.member_role in cred1.use_role.parents.all()
|
||||
assert objects.teams.team2.member_role in cred2.use_role.parents.all()
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_use_role(credential, user, permissions):
|
||||
@ -53,7 +55,7 @@ def test_credential_migration_team_member(credential, team, user, permissions):
|
||||
rbac.migrate_credential(apps, None)
|
||||
|
||||
# Admin permissions post migration
|
||||
assert u in credential.admin_role
|
||||
assert u in credential.use_role
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_migration_team_admin(credential, team, user, permissions):
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.access import NotificationTemplateAccess
|
||||
from awx.main.access import (
|
||||
NotificationTemplateAccess,
|
||||
NotificationAccess
|
||||
)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notification_template_get_queryset_orgmember(notification_template, user):
|
||||
@ -24,6 +27,11 @@ def test_notification_template_get_queryset_orgadmin(notification_template, user
|
||||
notification_template.organization.admin_role.members.add(user('admin', False))
|
||||
assert access.get_queryset().count() == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notification_template_get_queryset_org_auditor(notification_template, org_auditor):
|
||||
access = NotificationTemplateAccess(org_auditor)
|
||||
assert access.get_queryset().count() == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notification_template_access_superuser(notification_template_factory):
|
||||
nf_objects = notification_template_factory('test-orphaned', organization='test', superusers=['admin'])
|
||||
@ -81,3 +89,31 @@ def test_notificaiton_template_orphan_access_org_admin(notification_template, or
|
||||
notification_template.organization = None
|
||||
access = NotificationTemplateAccess(org_admin)
|
||||
assert not access.can_change(notification_template, {'organization': organization.id})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notification_access_get_queryset_org_admin(notification, org_admin):
|
||||
access = NotificationAccess(org_admin)
|
||||
assert access.get_queryset().count() == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notification_access_get_queryset_org_auditor(notification, org_auditor):
|
||||
access = NotificationAccess(org_auditor)
|
||||
assert access.get_queryset().count() == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notification_access_system_admin(notification, admin):
|
||||
access = NotificationAccess(admin)
|
||||
assert access.can_read(notification)
|
||||
assert access.can_delete(notification)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notification_access_org_admin(notification, org_admin):
|
||||
access = NotificationAccess(org_admin)
|
||||
assert access.can_read(notification)
|
||||
assert access.can_delete(notification)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notification_access_org_auditor(notification, org_auditor):
|
||||
access = NotificationAccess(org_auditor)
|
||||
assert access.can_read(notification)
|
||||
assert not access.can_delete(notification)
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import os
|
||||
import unittest2 as unittest
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import LiveServerTestCase
|
||||
from django.test.utils import override_settings
|
||||
@ -8,6 +11,7 @@ from django.test.utils import override_settings
|
||||
from awx.main.tests.job_base import BaseJobTestMixin
|
||||
|
||||
|
||||
@unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test')
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True,
|
||||
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
|
||||
ANSIBLE_TRANSPORT='local')
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
|
||||
# Python
|
||||
import mock
|
||||
|
||||
# Django
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.ha import * # noqa
|
||||
|
||||
__all__ = ['HAUnitTest',]
|
||||
|
||||
class HAUnitTest(SimpleTestCase):
|
||||
|
||||
@mock.patch('awx.main.models.Instance.objects.count', return_value=2)
|
||||
def test_multiple_instances(self, ignore):
|
||||
self.assertTrue(is_ha_environment())
|
||||
|
||||
@mock.patch('awx.main.models.Instance.objects.count', return_value=1)
|
||||
def test_db_localhost(self, ignore):
|
||||
self.assertFalse(is_ha_environment())
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
# Python
|
||||
from __future__ import absolute_import
|
||||
import json
|
||||
import os
|
||||
import unittest2 as unittest
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import reverse
|
||||
@ -15,6 +17,7 @@ from awx.main.tests.job_base import BaseJobTestMixin
|
||||
|
||||
__all__ = ['JobRelaunchTest',]
|
||||
|
||||
@unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test')
|
||||
class JobRelaunchTest(BaseJobTestMixin, BaseLiveServerTest):
|
||||
|
||||
def test_job_relaunch(self):
|
||||
|
||||
@ -1,147 +0,0 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from awx.main.models import Host, Inventory, Organization
|
||||
from awx.main.tests.base import BaseTest
|
||||
import awx.main.task_engine
|
||||
from awx.main.task_engine import * # noqa
|
||||
|
||||
class LicenseTests(BaseTest):
|
||||
|
||||
def setUp(self):
|
||||
self.start_redis()
|
||||
self.setup_instances()
|
||||
super(LicenseTests, self).setUp()
|
||||
self.setup_users()
|
||||
u = self.super_django_user
|
||||
org = Organization.objects.create(name='o1', created_by=u)
|
||||
org.admin_role.members.add(self.normal_django_user)
|
||||
self.inventory = Inventory.objects.create(name='hi', organization=org, created_by=u)
|
||||
Host.objects.create(name='a1', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a2', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a3', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a4', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a5', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a6', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a7', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a8', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a9', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a10', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a11', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a12', inventory=self.inventory, created_by=u)
|
||||
self._temp_task_file = awx.main.task_engine.TEMPORARY_TASK_FILE
|
||||
self._temp_task_fetch_ami = awx.main.task_engine.TemporaryTaskEngine.fetch_ami
|
||||
self._temp_task_fetch_instance = awx.main.task_engine.TemporaryTaskEngine.fetch_instance
|
||||
|
||||
def tearDown(self):
|
||||
awx.main.task_engine.TEMPORARY_TASK_FILE = self._temp_task_file
|
||||
awx.main.task_engine.TemporaryTaskEngine.fetch_ami = self._temp_task_fetch_ami
|
||||
awx.main.task_engine.TemporaryTaskEngine.fetch_instance = self._temp_task_fetch_instance
|
||||
super(LicenseTests, self).tearDown()
|
||||
self.stop_redis()
|
||||
|
||||
def test_license_writer(self):
|
||||
|
||||
writer = TaskEngager(
|
||||
company_name='acmecorp',
|
||||
contact_name='Michael DeHaan',
|
||||
contact_email='michael@ansibleworks.com',
|
||||
license_date=25000, # seconds since epoch
|
||||
instance_count=500)
|
||||
|
||||
data = writer.get_data()
|
||||
|
||||
assert data['instance_count'] == 500
|
||||
assert data['contact_name'] == 'Michael DeHaan'
|
||||
assert data['contact_email'] == 'michael@ansibleworks.com'
|
||||
assert data['license_date'] == 25000
|
||||
assert data['license_key'] == "11bae31f31c6a6cdcb483a278cdbe98bd8ac5761acd7163a50090b0f098b3a13"
|
||||
|
||||
strdata = writer.get_string()
|
||||
strdata_loaded = json.loads(strdata)
|
||||
assert strdata_loaded == data
|
||||
|
||||
reader = TaskSerializer()
|
||||
|
||||
vdata = reader.from_string(strdata)
|
||||
|
||||
assert vdata['available_instances'] == 500
|
||||
assert vdata['current_instances'] == 12
|
||||
assert vdata['free_instances'] == 488
|
||||
assert vdata['date_warning'] is True
|
||||
assert vdata['date_expired'] is True
|
||||
assert vdata['license_date'] == 25000
|
||||
assert vdata['time_remaining'] < 0
|
||||
assert vdata['valid_key'] is True
|
||||
assert vdata['compliant'] is False
|
||||
assert vdata['subscription_name']
|
||||
|
||||
def test_expired_licenses(self):
|
||||
reader = TaskSerializer()
|
||||
writer = TaskEngager(
|
||||
company_name='Tower',
|
||||
contact_name='Tower Admin',
|
||||
contact_email='tower@ansible.com',
|
||||
license_date=int(time.time() - 3600),
|
||||
instance_count=100,
|
||||
trial=True)
|
||||
strdata = writer.get_string()
|
||||
vdata = reader.from_string(strdata)
|
||||
|
||||
assert vdata['compliant'] is False
|
||||
assert vdata['grace_period_remaining'] < 0
|
||||
|
||||
writer = TaskEngager(
|
||||
company_name='Tower',
|
||||
contact_name='Tower Admin',
|
||||
contact_email='tower@ansible.com',
|
||||
license_date=int(time.time() - 2592001),
|
||||
instance_count=100,
|
||||
trial=False)
|
||||
strdata = writer.get_string()
|
||||
vdata = reader.from_string(strdata)
|
||||
|
||||
assert vdata['compliant'] is False
|
||||
assert vdata['grace_period_remaining'] < 0
|
||||
|
||||
writer = TaskEngager(
|
||||
company_name='Tower',
|
||||
contact_name='Tower Admin',
|
||||
contact_email='tower@ansible.com',
|
||||
license_date=int(time.time() - 3600),
|
||||
instance_count=100,
|
||||
trial=False)
|
||||
strdata = writer.get_string()
|
||||
vdata = reader.from_string(strdata)
|
||||
|
||||
assert vdata['compliant'] is False
|
||||
assert vdata['grace_period_remaining'] > 0
|
||||
|
||||
def test_aws_license(self):
|
||||
os.environ['AWX_LICENSE_FILE'] = 'non-existent-license-file.json'
|
||||
h, path = tempfile.mkstemp()
|
||||
self._temp_paths.append(path)
|
||||
with os.fdopen(h, 'w') as f:
|
||||
json.dump({'instance_count': 100}, f)
|
||||
awx.main.task_engine.TEMPORARY_TASK_FILE = path
|
||||
|
||||
def fetch_ami(_self):
|
||||
_self.attributes['ami-id'] = 'ami-00000000'
|
||||
return True
|
||||
|
||||
def fetch_instance(_self):
|
||||
_self.attributes['instance-id'] = 'i-00000000'
|
||||
return True
|
||||
|
||||
awx.main.task_engine.TemporaryTaskEngine.fetch_ami = fetch_ami
|
||||
awx.main.task_engine.TemporaryTaskEngine.fetch_instance = fetch_instance
|
||||
reader = TaskSerializer()
|
||||
license = reader.from_file()
|
||||
self.assertTrue(license['is_aws'])
|
||||
self.assertTrue(license['time_remaining'])
|
||||
self.assertTrue(license['free_instances'] > 0)
|
||||
self.assertTrue(license['grace_period_remaining'] > 0)
|
||||
@ -1,413 +0,0 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
from datetime import timedelta
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.timezone import now as tz_now
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.tests.base import BaseTest
|
||||
|
||||
__all__ = ['AuthTokenLimitUnitTest', 'OrganizationsTest']
|
||||
|
||||
class AuthTokenLimitUnitTest(BaseTest):
|
||||
|
||||
def setUp(self):
|
||||
self.now = tz_now()
|
||||
# Times are relative to now
|
||||
# (key, created on in seconds , expiration in seconds)
|
||||
self.test_data = [
|
||||
# a is implicitly expired
|
||||
("a", -1000, -10),
|
||||
# b's are invalid due to session limit of 3
|
||||
("b", -100, 60),
|
||||
("bb", -100, 60),
|
||||
("c", -90, 70),
|
||||
("d", -80, 80),
|
||||
("e", -70, 90),
|
||||
]
|
||||
self.user = User.objects.create_superuser('admin', 'foo@bar.com', 'password')
|
||||
for key, t_create, t_expire in self.test_data:
|
||||
AuthToken.objects.create(
|
||||
user=self.user,
|
||||
key=key,
|
||||
request_hash='this_is_a_hash',
|
||||
created=self.now + timedelta(seconds=t_create),
|
||||
expires=self.now + timedelta(seconds=t_expire),
|
||||
)
|
||||
super(AuthTokenLimitUnitTest, self).setUp()
|
||||
|
||||
@override_settings(AUTH_TOKEN_PER_USER=3)
|
||||
def test_get_tokens_over_limit(self):
|
||||
invalid_tokens = AuthToken.get_tokens_over_limit(self.user, now=self.now)
|
||||
invalid_keys = [x.key for x in invalid_tokens]
|
||||
self.assertEqual(len(invalid_keys), 2)
|
||||
self.assertIn('b', invalid_keys)
|
||||
self.assertIn('bb', invalid_keys)
|
||||
|
||||
class OrganizationsTest(BaseTest):
|
||||
|
||||
def collection(self):
|
||||
return reverse('api:organization_list')
|
||||
|
||||
def setUp(self):
|
||||
super(OrganizationsTest, self).setUp()
|
||||
self.setup_instances()
|
||||
# TODO: Test non-enterprise license
|
||||
self.create_test_license_file()
|
||||
self.setup_users()
|
||||
|
||||
self.organizations = self.make_organizations(self.super_django_user, 10)
|
||||
self.projects = self.make_projects(self.normal_django_user, 10)
|
||||
|
||||
# add projects to organizations in a more or less arbitrary way
|
||||
for project in self.projects[0:2]:
|
||||
self.organizations[0].projects.add(project)
|
||||
for project in self.projects[3:8]:
|
||||
self.organizations[1].projects.add(project)
|
||||
for project in self.projects[9:10]:
|
||||
self.organizations[2].projects.add(project)
|
||||
self.organizations[0].projects.add(self.projects[-1])
|
||||
self.organizations[9].projects.add(self.projects[-2])
|
||||
|
||||
# get the URL for various organization records
|
||||
self.a_detail_url = "%s%s" % (self.collection(), self.organizations[0].pk)
|
||||
self.b_detail_url = "%s%s" % (self.collection(), self.organizations[1].pk)
|
||||
self.c_detail_url = "%s%s" % (self.collection(), self.organizations[2].pk)
|
||||
|
||||
# configuration:
|
||||
# admin_user is an admin and regular user in all organizations
|
||||
# other_user is all organizations
|
||||
# normal_user is a user in organization 0, and an admin of organization 1
|
||||
# nobody_user is a user not a member of any organizations
|
||||
|
||||
for x in self.organizations:
|
||||
x.admin_role.members.add(self.super_django_user)
|
||||
x.member_role.members.add(self.super_django_user)
|
||||
x.member_role.members.add(self.other_django_user)
|
||||
|
||||
self.organizations[0].member_role.members.add(self.normal_django_user)
|
||||
self.organizations[1].admin_role.members.add(self.normal_django_user)
|
||||
|
||||
def test_get_organization_list(self):
|
||||
url = reverse('api:organization_list')
|
||||
|
||||
# no credentials == 401
|
||||
self.options(url, expect=401)
|
||||
self.head(url, expect=401)
|
||||
self.get(url, expect=401)
|
||||
|
||||
# wrong credentials == 401
|
||||
with self.current_user(self.get_invalid_credentials()):
|
||||
self.options(url, expect=401)
|
||||
self.head(url, expect=401)
|
||||
self.get(url, expect=401)
|
||||
|
||||
# superuser credentials == 200, full list
|
||||
with self.current_user(self.super_django_user):
|
||||
self.options(url, expect=200)
|
||||
self.head(url, expect=200)
|
||||
response = self.get(url, expect=200)
|
||||
self.check_pagination_and_size(response, 10, previous=None, next=None)
|
||||
self.assertEqual(len(response['results']),
|
||||
Organization.objects.count())
|
||||
for field in ['id', 'url', 'name', 'description', 'created']:
|
||||
self.assertTrue(field in response['results'][0],
|
||||
'field %s not in result' % field)
|
||||
|
||||
# check that the related URL functionality works
|
||||
related = response['results'][0]['related']
|
||||
for x in ['projects', 'users', 'admins']:
|
||||
self.assertTrue(x in related and related[x].endswith("/%s/" % x), "looking for %s in related" % x)
|
||||
|
||||
# normal credentials == 200, get only organizations of which user is a member
|
||||
with self.current_user(self.normal_django_user):
|
||||
self.options(url, expect=200)
|
||||
self.head(url, expect=200)
|
||||
response = self.get(url, expect=200)
|
||||
self.check_pagination_and_size(response, 2, previous=None, next=None)
|
||||
|
||||
# no admin rights? get empty list
|
||||
with self.current_user(self.other_django_user):
|
||||
response = self.get(url, expect=200)
|
||||
self.check_pagination_and_size(response, len(self.organizations), previous=None, next=None)
|
||||
|
||||
# not a member of any orgs? get empty list
|
||||
with self.current_user(self.nobody_django_user):
|
||||
response = self.get(url, expect=200)
|
||||
self.check_pagination_and_size(response, 0, previous=None, next=None)
|
||||
|
||||
def test_get_item(self):
|
||||
|
||||
# first get all the URLs
|
||||
data = self.get(self.collection(), expect=200, auth=self.get_super_credentials())
|
||||
urls = [item['url'] for item in data['results']]
|
||||
|
||||
# make sure super user can fetch records
|
||||
data = self.get(urls[0], expect=200, auth=self.get_super_credentials())
|
||||
[self.assertTrue(key in data) for key in ['name', 'description', 'url']]
|
||||
|
||||
# make sure invalid user cannot
|
||||
data = self.get(urls[0], expect=401, auth=self.get_invalid_credentials())
|
||||
|
||||
# normal user should be able to get org 0 and org 1 but not org 9 (as he's not a user or admin of it)
|
||||
data = self.get(urls[0], expect=200, auth=self.get_normal_credentials())
|
||||
data = self.get(urls[1], expect=200, auth=self.get_normal_credentials())
|
||||
data = self.get(urls[9], expect=403, auth=self.get_normal_credentials())
|
||||
|
||||
# other user is a member, but not admin, can access org
|
||||
data = self.get(urls[0], expect=200, auth=self.get_other_credentials())
|
||||
|
||||
# nobody user is not a member, cannot access org
|
||||
data = self.get(urls[0], expect=403, auth=self.get_nobody_credentials())
|
||||
|
||||
def test_get_item_subobjects_projects(self):
|
||||
|
||||
# first get all the orgs
|
||||
orgs = self.get(self.collection(), expect=200, auth=self.get_super_credentials())
|
||||
|
||||
# find projects attached to the first org
|
||||
projects0_url = orgs['results'][0]['related']['projects']
|
||||
projects1_url = orgs['results'][1]['related']['projects']
|
||||
projects9_url = orgs['results'][9]['related']['projects']
|
||||
|
||||
self.get(projects0_url, expect=401, auth=None)
|
||||
self.get(projects0_url, expect=401, auth=self.get_invalid_credentials())
|
||||
|
||||
# normal user is just a member of the first org, so can see all projects under the org
|
||||
self.get(projects0_url, expect=200, auth=self.get_normal_credentials())
|
||||
|
||||
# however in the second org, he's an admin and should see all of them
|
||||
projects1a = self.get(projects1_url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(projects1a['count'], 5)
|
||||
|
||||
# but the non-admin cannot access the list of projects in the org. He should use /projects/ instead!
|
||||
self.get(projects1_url, expect=200, auth=self.get_other_credentials())
|
||||
|
||||
# superuser should be able to read anything
|
||||
projects9a = self.get(projects9_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(projects9a['count'], 1)
|
||||
|
||||
# nobody user is not a member of any org, so can't see projects...
|
||||
self.get(projects0_url, expect=403, auth=self.get_nobody_credentials())
|
||||
projects1a = self.get(projects1_url, expect=403, auth=self.get_nobody_credentials())
|
||||
|
||||
def test_get_item_subobjects_users(self):
|
||||
|
||||
# see if we can list the users added to the organization
|
||||
orgs = self.get(self.collection(), expect=200, auth=self.get_super_credentials())
|
||||
org1_users_url = orgs['results'][1]['related']['users']
|
||||
org1_users = self.get(org1_users_url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(org1_users['count'], 2)
|
||||
org1_users = self.get(org1_users_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(org1_users['count'], 2)
|
||||
org1_users = self.get(org1_users_url, expect=200, auth=self.get_other_credentials())
|
||||
self.assertEquals(org1_users['count'], 2)
|
||||
|
||||
def test_get_item_subobjects_admins(self):
|
||||
|
||||
# see if we can list the users added to the organization
|
||||
orgs = self.get(self.collection(), expect=200, auth=self.get_super_credentials())
|
||||
org1_users_url = orgs['results'][1]['related']['admins']
|
||||
org1_users = self.get(org1_users_url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(org1_users['count'], 2)
|
||||
org1_users = self.get(org1_users_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(org1_users['count'], 2)
|
||||
|
||||
def test_get_organization_inventories_list(self):
|
||||
pass
|
||||
|
||||
def _test_get_item_subobjects_tags(self):
|
||||
# FIXME: Update to support taggit!
|
||||
|
||||
# put some tags on the org
|
||||
org1 = Organization.objects.get(pk=2)
|
||||
tag1 = Tag.objects.create(name='atag')
|
||||
tag2 = Tag.objects.create(name='btag')
|
||||
org1.tags.add(tag1)
|
||||
org1.tags.add(tag2)
|
||||
|
||||
# see if we can list the users added to the organization
|
||||
orgs = self.get(self.collection(), expect=200, auth=self.get_super_credentials())
|
||||
org1_tags_url = orgs['results'][1]['related']['tags']
|
||||
org1_tags = self.get(org1_tags_url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(org1_tags['count'], 2)
|
||||
org1_tags = self.get(org1_tags_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(org1_tags['count'], 2)
|
||||
org1_tags = self.get(org1_tags_url, expect=403, auth=self.get_other_credentials())
|
||||
|
||||
def _test_get_item_subobjects_audit_trail(self):
|
||||
# FIXME: Update to support whatever audit trail framework is used.
|
||||
url = '/api/v1/organizations/2/audit_trail/'
|
||||
self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
# FIXME: verify that some audit trail records are auto-created on save AND post
|
||||
|
||||
def test_post_item(self):
|
||||
|
||||
new_org = dict(name='magic test org', description='8675309')
|
||||
|
||||
# need to be a valid user
|
||||
self.post(self.collection(), new_org, expect=401, auth=None)
|
||||
self.post(self.collection(), new_org, expect=401, auth=self.get_invalid_credentials())
|
||||
|
||||
# only super users can create organizations
|
||||
self.post(self.collection(), new_org, expect=403, auth=self.get_normal_credentials())
|
||||
self.post(self.collection(), new_org, expect=403, auth=self.get_other_credentials())
|
||||
data1 = self.post(self.collection(), new_org, expect=201, auth=self.get_super_credentials())
|
||||
|
||||
# duplicate post results in 400
|
||||
response = self.post(self.collection(), new_org, expect=400, auth=self.get_super_credentials())
|
||||
self.assertTrue('name' in response, response)
|
||||
self.assertTrue('Name' in response['name'][0], response)
|
||||
|
||||
# look at what we got back from the post, make sure we added an org
|
||||
last_org = Organization.objects.order_by('-pk')[0]
|
||||
self.assertTrue(data1['url'].endswith("/%d/" % last_org.pk))
|
||||
|
||||
# Test that not even super users can create an organization with a basic license
|
||||
self.create_basic_license_file()
|
||||
cant_org = dict(name='silly user org', description='4815162342')
|
||||
self.post(self.collection(), cant_org, expect=402, auth=self.get_super_credentials())
|
||||
|
||||
def test_post_item_subobjects_users(self):
|
||||
|
||||
url = reverse('api:organization_users_list', args=(self.organizations[1].pk,))
|
||||
users = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(users['count'], 2)
|
||||
self.post(url, dict(id=self.normal_django_user.pk), expect=204, auth=self.get_normal_credentials())
|
||||
users = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(users['count'], 3)
|
||||
self.post(url, dict(id=self.normal_django_user.pk, disassociate=True), expect=204, auth=self.get_normal_credentials())
|
||||
users = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(users['count'], 2)
|
||||
|
||||
# post a completely new user to verify we can add users to the subcollection directly
|
||||
new_user = dict(username='NewUser9000', password='NewPassword9000')
|
||||
which_org = Organization.accessible_objects(self.normal_django_user, 'admin_role')[0]
|
||||
url = reverse('api:organization_users_list', args=(which_org.pk,))
|
||||
self.post(url, new_user, expect=201, auth=self.get_normal_credentials())
|
||||
|
||||
all_users = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(all_users['count'], 3)
|
||||
|
||||
def test_post_item_subobjects_admins(self):
|
||||
|
||||
url = reverse('api:organization_admins_list', args=(self.organizations[1].pk,))
|
||||
admins = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(admins['count'], 2)
|
||||
self.post(url, dict(id=self.other_django_user.pk), expect=204, auth=self.get_normal_credentials())
|
||||
admins = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(admins['count'], 3)
|
||||
self.post(url, dict(id=self.other_django_user.pk, disassociate=1), expect=204, auth=self.get_normal_credentials())
|
||||
admins = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(admins['count'], 2)
|
||||
|
||||
def _test_post_item_subobjects_tags(self):
|
||||
# FIXME: Update to support taggit!
|
||||
|
||||
tag = Tag.objects.create(name='blippy')
|
||||
url = '/api/v1/organizations/2/tags/'
|
||||
tags = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(tags['count'], 0)
|
||||
self.post(url, dict(id=tag.pk), expect=204, auth=self.get_normal_credentials())
|
||||
tags = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(tags['count'], 1)
|
||||
self.assertEqual(tags['results'][0]['id'], tag.pk)
|
||||
self.post(url, dict(id=tag.pk, disassociate=1), expect=204, auth=self.get_normal_credentials())
|
||||
tags = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(tags['count'], 0)
|
||||
|
||||
def _test_post_item_subobjects_audit_trail(self):
|
||||
# FIXME: Update to support whatever audit trail framework is used.
|
||||
# audit trails are system things, and no user can post to them.
|
||||
url = '/api/v1/organizations/2/audit_trail/'
|
||||
self.post(url, dict(id=1), expect=405, auth=self.get_super_credentials())
|
||||
|
||||
def test_put_item(self):
|
||||
|
||||
# first get some urls and data to put back to them
|
||||
urls = self.get_urls(self.collection(), auth=self.get_super_credentials())
|
||||
self.get(urls[0], expect=200, auth=self.get_super_credentials())
|
||||
data1 = self.get(urls[1], expect=200, auth=self.get_super_credentials())
|
||||
|
||||
# test that an unauthenticated user cannot do a put
|
||||
new_data1 = data1.copy()
|
||||
new_data1['description'] = 'updated description'
|
||||
self.put(urls[0], new_data1, expect=401, auth=None)
|
||||
self.put(urls[0], new_data1, expect=401, auth=self.get_invalid_credentials())
|
||||
|
||||
# user normal is an admin of org 0 and a member of org 1 so should be able to put only org 1
|
||||
self.put(urls[0], new_data1, expect=403, auth=self.get_normal_credentials())
|
||||
self.put(urls[1], new_data1, expect=200, auth=self.get_normal_credentials())
|
||||
|
||||
# get back org 1 and see if it changed
|
||||
get_result = self.get(urls[1], expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(get_result['description'], 'updated description')
|
||||
|
||||
# super user can also put even though they aren't added to the org users or admins list
|
||||
self.put(urls[1], new_data1, expect=200, auth=self.get_super_credentials())
|
||||
|
||||
# make sure posting to this URL is not supported
|
||||
self.post(urls[1], new_data1, expect=405, auth=self.get_super_credentials())
|
||||
|
||||
def test_put_item_subobjects_projects(self):
|
||||
|
||||
# any attempt to put a subobject should be a 405, edit the actual resource or POST with 'disassociate' to delete
|
||||
# this is against a collection URL anyway, so we really need not repeat this test for other object types
|
||||
# as a PUT against a collection doesn't make much sense.
|
||||
|
||||
orgs = self.get(self.collection(), expect=200, auth=self.get_super_credentials())
|
||||
projects0_url = orgs['results'][0]['related']['projects']
|
||||
sub_projects = self.get(projects0_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(sub_projects['count'], 3)
|
||||
first_sub_project = sub_projects['results'][0]
|
||||
self.put(projects0_url, first_sub_project, expect=405, auth=self.get_super_credentials())
|
||||
|
||||
def test_delete_item(self):
|
||||
|
||||
# first get some urls
|
||||
urls = self.get_urls(self.collection(), auth=self.get_super_credentials())
|
||||
urldata1 = self.get(urls[1], auth=self.get_super_credentials())
|
||||
|
||||
# check authentication -- admins of the org and superusers can delete objects only
|
||||
self.delete(urls[0], expect=401, auth=None)
|
||||
self.delete(urls[0], expect=401, auth=self.get_invalid_credentials())
|
||||
self.delete(urls[8], expect=403, auth=self.get_normal_credentials())
|
||||
self.delete(urls[1], expect=204, auth=self.get_normal_credentials())
|
||||
self.delete(urls[0], expect=204, auth=self.get_super_credentials())
|
||||
|
||||
# check that when we have deleted an object it comes back 404 via GET
|
||||
self.get(urls[1], expect=404, auth=self.get_normal_credentials())
|
||||
assert Organization.objects.filter(pk=urldata1['id']).count() == 0
|
||||
|
||||
# also check that DELETE on the collection doesn't work
|
||||
self.delete(self.collection(), expect=405, auth=self.get_super_credentials())
|
||||
|
||||
# Test that not even super users can delete an organization with a basic license
|
||||
self.create_basic_license_file()
|
||||
self.delete(urls[2], expect=402, auth=self.get_super_credentials())
|
||||
|
||||
def test_invalid_post_data(self):
|
||||
url = reverse('api:organization_list')
|
||||
# API should gracefully handle data of an invalid type.
|
||||
self.post(url, expect=400, data=None, auth=self.get_super_credentials())
|
||||
self.post(url, expect=400, data=99, auth=self.get_super_credentials())
|
||||
self.post(url, expect=400, data='abcd', auth=self.get_super_credentials())
|
||||
self.post(url, expect=400, data=3.14, auth=self.get_super_credentials())
|
||||
self.post(url, expect=400, data=True, auth=self.get_super_credentials())
|
||||
self.post(url, expect=400, data=[1,2,3], auth=self.get_super_credentials())
|
||||
url = reverse('api:organization_users_list', args=(self.organizations[0].pk,))
|
||||
self.post(url, expect=400, data=None, auth=self.get_super_credentials())
|
||||
self.post(url, expect=400, data=99, auth=self.get_super_credentials())
|
||||
self.post(url, expect=400, data='abcd', auth=self.get_super_credentials())
|
||||
self.post(url, expect=400, data=3.14, auth=self.get_super_credentials())
|
||||
self.post(url, expect=400, data=True, auth=self.get_super_credentials())
|
||||
self.post(url, expect=400, data=[1,2,3], auth=self.get_super_credentials())
|
||||
|
||||
# TODO: tests for tag disassociation
|
||||
@ -1,55 +0,0 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved
|
||||
|
||||
# Python
|
||||
import mock
|
||||
from mock import Mock
|
||||
from StringIO import StringIO
|
||||
from django.utils.timezone import now
|
||||
|
||||
# Django
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
|
||||
__all__ = ['UnifiedJobsUnitTest',]
|
||||
|
||||
class UnifiedJobsUnitTest(SimpleTestCase):
|
||||
|
||||
# stdout file present
|
||||
@mock.patch('os.path.exists', return_value=True)
|
||||
@mock.patch('codecs.open', return_value='my_file_handler')
|
||||
def test_result_stdout_raw_handle_file__found(self, exists, open):
|
||||
unified_job = UnifiedJob()
|
||||
unified_job.result_stdout_file = 'dummy'
|
||||
|
||||
with mock.patch('os.stat', return_value=Mock(st_size=1)):
|
||||
result = unified_job.result_stdout_raw_handle()
|
||||
|
||||
self.assertEqual(result, 'my_file_handler')
|
||||
|
||||
# stdout file missing, job finished
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
def test_result_stdout_raw_handle__missing(self, exists):
|
||||
unified_job = UnifiedJob()
|
||||
unified_job.result_stdout_file = 'dummy'
|
||||
unified_job.finished = now()
|
||||
|
||||
result = unified_job.result_stdout_raw_handle()
|
||||
|
||||
self.assertIsInstance(result, StringIO)
|
||||
self.assertEqual(result.read(), 'stdout capture is missing')
|
||||
|
||||
# stdout file missing, job not finished
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
def test_result_stdout_raw_handle__pending(self, exists):
|
||||
unified_job = UnifiedJob()
|
||||
unified_job.result_stdout_file = 'dummy'
|
||||
unified_job.finished = None
|
||||
|
||||
result = unified_job.result_stdout_raw_handle()
|
||||
|
||||
self.assertIsInstance(result, StringIO)
|
||||
self.assertEqual(result.read(), 'Waiting for results...')
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
# Django
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
# Reuse Test code
|
||||
from awx.main.tests.base import (
|
||||
BaseLiveServerTest,
|
||||
QueueStartStopTestMixin,
|
||||
URI,
|
||||
)
|
||||
from awx.main.models.projects import * # noqa
|
||||
|
||||
__all__ = ['UnifiedJobStdoutRedactedTests']
|
||||
|
||||
|
||||
TEST_STDOUTS = []
|
||||
uri = URI(scheme="https", username="Dhh3U47nmC26xk9PKscV", password="PXPfWW8YzYrgS@E5NbQ2H@", host="github.ginger.com/theirrepo.git/info/refs")
|
||||
TEST_STDOUTS.append({
|
||||
'description': 'uri in a plain text document',
|
||||
'uri' : uri,
|
||||
'text' : 'hello world %s goodbye world' % uri,
|
||||
'occurrences' : 1
|
||||
})
|
||||
|
||||
uri = URI(scheme="https", username="applepie@@@", password="thatyouknow@@@@", host="github.ginger.com/theirrepo.git/info/refs")
|
||||
TEST_STDOUTS.append({
|
||||
'description': 'uri appears twice in a multiline plain text document',
|
||||
'uri' : uri,
|
||||
'text' : 'hello world %s \n\nyoyo\n\nhello\n%s' % (uri, uri),
|
||||
'occurrences' : 2
|
||||
})
|
||||
|
||||
class UnifiedJobStdoutRedactedTests(BaseLiveServerTest, QueueStartStopTestMixin):
|
||||
|
||||
def setUp(self):
|
||||
super(UnifiedJobStdoutRedactedTests, self).setUp()
|
||||
self.setup_instances()
|
||||
self.setup_users()
|
||||
self.test_cases = []
|
||||
self.negative_test_cases = []
|
||||
|
||||
proj = self.make_project()
|
||||
|
||||
for e in TEST_STDOUTS:
|
||||
e['project'] = ProjectUpdate(project=proj)
|
||||
e['project'].result_stdout_text = e['text']
|
||||
e['project'].save()
|
||||
self.test_cases.append(e)
|
||||
for d in TEST_STDOUTS:
|
||||
d['job'] = self.make_job()
|
||||
d['job'].result_stdout_text = d['text']
|
||||
d['job'].save()
|
||||
self.negative_test_cases.append(d)
|
||||
|
||||
# This is more of a functional test than a unit test.
|
||||
# should filter out username and password
|
||||
def check_sensitive_redacted(self, test_data, response):
|
||||
uri = test_data['uri']
|
||||
self.assertIsNotNone(response['content'])
|
||||
self.check_not_found(response['content'], uri.username, test_data['description'])
|
||||
self.check_not_found(response['content'], uri.password, test_data['description'])
|
||||
# Ensure the host didn't get redacted
|
||||
self.check_found(response['content'], uri.host, test_data['occurrences'], test_data['description'])
|
||||
|
||||
def check_sensitive_not_redacted(self, test_data, response):
|
||||
uri = test_data['uri']
|
||||
self.assertIsNotNone(response['content'])
|
||||
self.check_found(response['content'], uri.username, description=test_data['description'])
|
||||
self.check_found(response['content'], uri.password, description=test_data['description'])
|
||||
|
||||
def _get_url_job_stdout(self, job, url_base, format='json'):
|
||||
formats = {
|
||||
'json': 'application/json',
|
||||
'ansi': 'text/plain',
|
||||
'txt': 'text/plain',
|
||||
'html': 'text/html',
|
||||
}
|
||||
content_type = formats[format]
|
||||
project_update_stdout_url = reverse(url_base, args=(job.pk,)) + "?format=" + format
|
||||
return self.get(project_update_stdout_url, expect=200, auth=self.get_super_credentials(), accept=content_type)
|
||||
|
||||
def _test_redaction_enabled(self, format):
|
||||
for test_data in self.test_cases:
|
||||
response = self._get_url_job_stdout(test_data['project'], "api:project_update_stdout", format=format)
|
||||
self.check_sensitive_redacted(test_data, response)
|
||||
|
||||
def _test_redaction_disabled(self, format):
|
||||
for test_data in self.negative_test_cases:
|
||||
response = self._get_url_job_stdout(test_data['job'], "api:job_stdout", format=format)
|
||||
self.check_sensitive_not_redacted(test_data, response)
|
||||
|
||||
def test_project_update_redaction_enabled_json(self):
|
||||
self._test_redaction_enabled('json')
|
||||
|
||||
def test_project_update_redaction_enabled_ansi(self):
|
||||
self._test_redaction_enabled('ansi')
|
||||
|
||||
def test_project_update_redaction_enabled_html(self):
|
||||
self._test_redaction_enabled('html')
|
||||
|
||||
def test_project_update_redaction_enabled_txt(self):
|
||||
self._test_redaction_enabled('txt')
|
||||
|
||||
def test_job_redaction_disabled_json(self):
|
||||
self._test_redaction_disabled('json')
|
||||
|
||||
def test_job_redaction_disabled_ansi(self):
|
||||
self._test_redaction_disabled('ansi')
|
||||
|
||||
def test_job_redaction_disabled_html(self):
|
||||
self._test_redaction_disabled('html')
|
||||
|
||||
def test_job_redaction_disabled_txt(self):
|
||||
self._test_redaction_disabled('txt')
|
||||
15
awx/main/tests/unit/test_ha.py
Normal file
15
awx/main/tests/unit/test_ha.py
Normal file
@ -0,0 +1,15 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
|
||||
# Python
|
||||
import mock
|
||||
|
||||
# AWX
|
||||
from awx.main.ha import is_ha_environment
|
||||
|
||||
@mock.patch('awx.main.models.Instance.objects.count', lambda: 2)
|
||||
def test_multiple_instances():
|
||||
assert is_ha_environment()
|
||||
|
||||
@mock.patch('awx.main.models.Instance.objects.count', lambda: 1)
|
||||
def test_db_localhost():
|
||||
assert is_ha_environment() is False
|
||||
@ -1,11 +1,8 @@
|
||||
|
||||
import textwrap
|
||||
|
||||
# AWX
|
||||
from awx.main.redact import UriCleaner
|
||||
from awx.main.tests.base import BaseTest, URI
|
||||
|
||||
__all__ = ['UriCleanTests']
|
||||
from awx.main.tests.URI import URI
|
||||
|
||||
TEST_URIS = [
|
||||
URI('no host', scheme='https', username='myusername', password='mypass', host=None),
|
||||
@ -80,59 +77,58 @@ TEST_CLEARTEXT.append({
|
||||
'host_occurrences' : 4
|
||||
})
|
||||
|
||||
class UriCleanTests(BaseTest):
|
||||
|
||||
# should redact sensitive usernames and passwords
|
||||
def test_uri_scm_simple_redacted(self):
|
||||
for uri in TEST_URIS:
|
||||
redacted_str = UriCleaner.remove_sensitive(str(uri))
|
||||
if uri.username:
|
||||
self.check_not_found(redacted_str, uri.username, uri.description)
|
||||
if uri.password:
|
||||
self.check_not_found(redacted_str, uri.password, uri.description)
|
||||
|
||||
# should replace secret data with safe string, UriCleaner.REPLACE_STR
|
||||
def test_uri_scm_simple_replaced(self):
|
||||
for uri in TEST_URIS:
|
||||
redacted_str = UriCleaner.remove_sensitive(str(uri))
|
||||
self.check_found(redacted_str, UriCleaner.REPLACE_STR, uri.get_secret_count())
|
||||
|
||||
# should redact multiple uris in text
|
||||
def test_uri_scm_multiple(self):
|
||||
cleartext = ''
|
||||
for uri in TEST_URIS:
|
||||
cleartext += str(uri) + ' '
|
||||
for uri in TEST_URIS:
|
||||
cleartext += str(uri) + '\n'
|
||||
|
||||
# should redact sensitive usernames and passwords
|
||||
def test_uri_scm_simple_redacted():
|
||||
for uri in TEST_URIS:
|
||||
redacted_str = UriCleaner.remove_sensitive(str(uri))
|
||||
if uri.username:
|
||||
self.check_not_found(redacted_str, uri.username, uri.description)
|
||||
assert uri.username not in redacted_str
|
||||
if uri.password:
|
||||
self.check_not_found(redacted_str, uri.password, uri.description)
|
||||
assert uri.username not in redacted_str
|
||||
|
||||
# should replace multiple secret data with safe string
|
||||
def test_uri_scm_multiple_replaced(self):
|
||||
cleartext = ''
|
||||
find_count = 0
|
||||
for uri in TEST_URIS:
|
||||
cleartext += str(uri) + ' '
|
||||
find_count += uri.get_secret_count()
|
||||
# should replace secret data with safe string, UriCleaner.REPLACE_STR
|
||||
def test_uri_scm_simple_replaced():
|
||||
for uri in TEST_URIS:
|
||||
redacted_str = UriCleaner.remove_sensitive(str(uri))
|
||||
assert redacted_str.count(UriCleaner.REPLACE_STR) == uri.get_secret_count()
|
||||
|
||||
for uri in TEST_URIS:
|
||||
cleartext += str(uri) + '\n'
|
||||
find_count += uri.get_secret_count()
|
||||
# should redact multiple uris in text
|
||||
def test_uri_scm_multiple():
|
||||
cleartext = ''
|
||||
for uri in TEST_URIS:
|
||||
cleartext += str(uri) + ' '
|
||||
for uri in TEST_URIS:
|
||||
cleartext += str(uri) + '\n'
|
||||
|
||||
redacted_str = UriCleaner.remove_sensitive(cleartext)
|
||||
self.check_found(redacted_str, UriCleaner.REPLACE_STR, find_count)
|
||||
redacted_str = UriCleaner.remove_sensitive(str(uri))
|
||||
if uri.username:
|
||||
assert uri.username not in redacted_str
|
||||
if uri.password:
|
||||
assert uri.username not in redacted_str
|
||||
|
||||
# should redact and replace multiple secret data within a complex cleartext blob
|
||||
def test_uri_scm_cleartext_redact_and_replace(self):
|
||||
for test_data in TEST_CLEARTEXT:
|
||||
uri = test_data['uri']
|
||||
redacted_str = UriCleaner.remove_sensitive(test_data['text'])
|
||||
self.check_not_found(redacted_str, uri.username, uri.description)
|
||||
self.check_not_found(redacted_str, uri.password, uri.description)
|
||||
# Ensure the host didn't get redacted
|
||||
self.check_found(redacted_str, uri.host, test_data['host_occurrences'], uri.description)
|
||||
# should replace multiple secret data with safe string
|
||||
def test_uri_scm_multiple_replaced():
|
||||
cleartext = ''
|
||||
find_count = 0
|
||||
for uri in TEST_URIS:
|
||||
cleartext += str(uri) + ' '
|
||||
find_count += uri.get_secret_count()
|
||||
|
||||
for uri in TEST_URIS:
|
||||
cleartext += str(uri) + '\n'
|
||||
find_count += uri.get_secret_count()
|
||||
|
||||
redacted_str = UriCleaner.remove_sensitive(cleartext)
|
||||
assert redacted_str.count(UriCleaner.REPLACE_STR) == find_count
|
||||
|
||||
# should redact and replace multiple secret data within a complex cleartext blob
|
||||
def test_uri_scm_cleartext_redact_and_replace():
|
||||
for test_data in TEST_CLEARTEXT:
|
||||
uri = test_data['uri']
|
||||
redacted_str = UriCleaner.remove_sensitive(test_data['text'])
|
||||
assert uri.username not in redacted_str
|
||||
assert uri.password not in redacted_str
|
||||
# Ensure the host didn't get redacted
|
||||
assert redacted_str.count(uri.host) == test_data['host_occurrences']
|
||||
48
awx/main/tests/unit/test_unified_jobs.py
Normal file
48
awx/main/tests/unit/test_unified_jobs.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved
|
||||
|
||||
# Python
|
||||
import mock
|
||||
from mock import Mock
|
||||
from StringIO import StringIO
|
||||
from django.utils.timezone import now
|
||||
|
||||
# AWX
|
||||
from awx.main.models import UnifiedJob
|
||||
|
||||
|
||||
# stdout file present
|
||||
@mock.patch('os.path.exists', return_value=True)
|
||||
@mock.patch('codecs.open', return_value='my_file_handler')
|
||||
def test_result_stdout_raw_handle_file__found(exists, open):
|
||||
unified_job = UnifiedJob()
|
||||
unified_job.result_stdout_file = 'dummy'
|
||||
|
||||
with mock.patch('os.stat', return_value=Mock(st_size=1)):
|
||||
result = unified_job.result_stdout_raw_handle()
|
||||
|
||||
assert result == 'my_file_handler'
|
||||
|
||||
# stdout file missing, job finished
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
def test_result_stdout_raw_handle__missing(exists):
|
||||
unified_job = UnifiedJob()
|
||||
unified_job.result_stdout_file = 'dummy'
|
||||
unified_job.finished = now()
|
||||
|
||||
result = unified_job.result_stdout_raw_handle()
|
||||
|
||||
assert isinstance(result, StringIO)
|
||||
assert result.read() == 'stdout capture is missing'
|
||||
|
||||
# stdout file missing, job not finished
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
def test_result_stdout_raw_handle__pending(exists):
|
||||
unified_job = UnifiedJob()
|
||||
unified_job.result_stdout_file = 'dummy'
|
||||
unified_job.finished = None
|
||||
|
||||
result = unified_job.result_stdout_raw_handle()
|
||||
|
||||
assert isinstance(result, StringIO)
|
||||
assert result.read() == 'Waiting for results...'
|
||||
@ -25,6 +25,7 @@ DATABASES = {
|
||||
'NAME': 'awx-dev',
|
||||
'USER': 'awx-dev',
|
||||
'PASSWORD': 'AWXsome1',
|
||||
'ATOMIC_REQUESTS': True,
|
||||
'HOST': 'postgres',
|
||||
'PORT': '',
|
||||
}
|
||||
|
||||
@ -80,6 +80,11 @@ table, tbody {
|
||||
border-top:0px!important;
|
||||
}
|
||||
|
||||
.List-tableCell.description-column {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.List-actionButtonCell {
|
||||
padding-top:5px;
|
||||
padding-right: 15px;
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
@import "src/shared/text-label.less";
|
||||
@import "src/shared/branding/colors.default.less";
|
||||
|
||||
.host-disabled-label {
|
||||
&:after {
|
||||
.include-text-label(#676767; white; "disabled");
|
||||
display: inline-block;
|
||||
content: "disabled";
|
||||
border-radius: 3px;
|
||||
color: @default-icon;
|
||||
text-transform: uppercase;
|
||||
font-size: .7em;
|
||||
font-style: normal;
|
||||
margin-left: 0.5em;
|
||||
padding: 0.35em;
|
||||
padding-bottom: 0.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,17 +105,17 @@
|
||||
</div>
|
||||
<div class="AddPermissions-footer">
|
||||
<div class="buttons Form-buttons AddPermissions-buttons">
|
||||
<button type="button"
|
||||
class="btn btn-sm Form-cancelButton"
|
||||
ng-click="closeModal()">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-sm Form-saveButton"
|
||||
ng-click="updatePermissions()"
|
||||
ng-disabled="userRoleForm.$invalid || !allSelected || !allSelected.length">
|
||||
Save
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-sm Form-cancelButton"
|
||||
ng-click="closeModal()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -53,6 +53,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.RoleList-tag--team {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.RoleList-tagDelete {
|
||||
font-size: 13px;
|
||||
color: @default-bg;
|
||||
|
||||
@ -7,8 +7,10 @@
|
||||
class="fa fa-times RoleList-tagDelete"></i>
|
||||
</div>
|
||||
<div class="RoleList-tag"
|
||||
ng-class="{'RoleList-tag--deletable': entry.explicit}">
|
||||
ng-class="{'RoleList-tag--deletable': entry.explicit,
|
||||
'RoleList-tag--team': entry.team_id}"
|
||||
aw-tool-tip='{{entry.team_name | sanitize}}' aw-tip-placement='bottom'>
|
||||
<span class="RoleList-name">{{ entry.name }}</span>
|
||||
<i ng-show='entry.team_id' class="fa fa-users" ng-attr-title='{{entry.team_name}}'></i>
|
||||
<i ng-show='entry.team_id' class="fa fa-users"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -255,6 +255,16 @@ var tower = angular.module('Tower', [
|
||||
});
|
||||
});
|
||||
}]
|
||||
},
|
||||
onExit: function(){
|
||||
// close the job launch modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
// Destroy the dialog
|
||||
if($("#job-launch-modal").hasClass('ui-dialog-content')) {
|
||||
$('#job-launch-modal').dialog('destroy');
|
||||
}
|
||||
// Remove the directive from the page (if it's there)
|
||||
$('#content-container').find('submit-job').remove();
|
||||
}
|
||||
}).
|
||||
|
||||
@ -264,6 +274,16 @@ var tower = angular.module('Tower', [
|
||||
controller: JobsListController,
|
||||
ncyBreadcrumb: {
|
||||
label: "JOBS"
|
||||
},
|
||||
onExit: function(){
|
||||
// close the job launch modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
// Destroy the dialog
|
||||
if($("#job-launch-modal").hasClass('ui-dialog-content')) {
|
||||
$('#job-launch-modal').dialog('destroy');
|
||||
}
|
||||
// Remove the directive from the page (if it's there)
|
||||
$('#content-container').find('submit-job').remove();
|
||||
}
|
||||
}).
|
||||
|
||||
@ -521,11 +541,12 @@ var tower = angular.module('Tower', [
|
||||
'ClearScope', 'Socket', 'LoadConfig', 'Store',
|
||||
'ShowSocketHelp', 'pendoService', 'Prompt', 'Rest', 'Wait',
|
||||
'ProcessErrors', '$state', 'GetBasePath', 'ConfigService',
|
||||
'FeaturesService',
|
||||
'FeaturesService', '$filter',
|
||||
function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense,
|
||||
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
|
||||
LoadConfig, Store, ShowSocketHelp, pendoService, Prompt, Rest, Wait,
|
||||
ProcessErrors, $state, GetBasePath, ConfigService, FeaturesService) {
|
||||
ProcessErrors, $state, GetBasePath, ConfigService, FeaturesService,
|
||||
$filter) {
|
||||
var sock;
|
||||
$rootScope.addPermission = function (scope) {
|
||||
$compile("<add-permissions class='AddPermissions'></add-permissions>")(scope);
|
||||
@ -563,7 +584,7 @@ var tower = angular.module('Tower', [
|
||||
if (accessListEntry.team_id) {
|
||||
Prompt({
|
||||
hdr: `Team access removal`,
|
||||
body: `<div class="Prompt-bodyQuery">Please confirm that you would like to remove <span class="Prompt-emphasis">${entry.name}</span> access from the team <span class="Prompt-emphasis">${entry.team_name}</span>. This will affect all members of the team. If you would like to only remove access for this particular user, please remove them from the team.</div>`,
|
||||
body: `<div class="Prompt-bodyQuery">Please confirm that you would like to remove <span class="Prompt-emphasis">${entry.name}</span> access from the team <span class="Prompt-emphasis">${$filter('sanitize')(entry.team_name)}</span>. This will affect all members of the team. If you would like to only remove access for this particular user, please remove them from the team.</div>`,
|
||||
action: action,
|
||||
actionText: 'REMOVE TEAM ACCESS'
|
||||
});
|
||||
@ -859,6 +880,7 @@ var tower = angular.module('Tower', [
|
||||
$rootScope.$broadcast("EditIndicatorChange", list, id);
|
||||
} else if ($rootScope.addedAnItem) {
|
||||
delete $rootScope.addedAnItem;
|
||||
$rootScope.$broadcast("RemoveIndicator");
|
||||
} else {
|
||||
$rootScope.$broadcast("RemoveIndicator");
|
||||
}
|
||||
@ -879,6 +901,7 @@ var tower = angular.module('Tower', [
|
||||
}
|
||||
// If browser refresh, set the user_is_superuser value
|
||||
$rootScope.user_is_superuser = Authorization.getUserInfo('is_superuser');
|
||||
$rootScope.user_is_system_auditor = Authorization.getUserInfo('is_system_auditor');
|
||||
// state the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket)
|
||||
if(!_.contains($location.$$url, '/login')){
|
||||
ConfigService.getConfig().then(function(){
|
||||
|
||||
@ -6,7 +6,7 @@ export default
|
||||
templateUrl: templateUrl('bread-crumb/bread-crumb'),
|
||||
link: function(scope) {
|
||||
|
||||
var streamConfig = {};
|
||||
var streamConfig = {}, originalRoute;
|
||||
|
||||
scope.showActivityStreamButton = false;
|
||||
scope.showRefreshButton = false;
|
||||
@ -30,16 +30,15 @@ export default
|
||||
stateGoParams.id = $state.params[streamConfig.activityStreamId];
|
||||
}
|
||||
}
|
||||
|
||||
originalRoute = $state.current;
|
||||
$state.go('activityStream', stateGoParams);
|
||||
}
|
||||
// The user is navigating away from the activity stream - take them back from whence they came
|
||||
else {
|
||||
// Pull the previous state out of local storage
|
||||
var previousState = Store('previous_state');
|
||||
|
||||
if(previousState && !Empty(previousState.name)) {
|
||||
$state.go(previousState.name, previousState.fromParams);
|
||||
if(originalRoute) {
|
||||
$state.go(originalRoute.name, originalRoute.fromParams);
|
||||
}
|
||||
else {
|
||||
// If for some reason something went wrong (like local storage was wiped, etc) take the
|
||||
|
||||
@ -391,6 +391,10 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l
|
||||
defaultUrl = GetBasePath('projects'),
|
||||
master = {};
|
||||
|
||||
// remove "type" field from search options
|
||||
CredentialList = _.cloneDeep(CredentialList);
|
||||
CredentialList.fields.kind.noSearch = true;
|
||||
|
||||
generator.inject(form, { mode: 'add', related: false, scope: $scope });
|
||||
generator.reset();
|
||||
|
||||
@ -567,6 +571,11 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
|
||||
id = $stateParams.id,
|
||||
relatedSets = {};
|
||||
|
||||
// remove "type" field from search options
|
||||
CredentialList = _.cloneDeep(CredentialList);
|
||||
CredentialList.fields.kind.noSearch = true;
|
||||
|
||||
|
||||
SchedulesList.well = false;
|
||||
generator.inject(form, {
|
||||
mode: 'edit',
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
$scope.formSave = function(){
|
||||
var host = {
|
||||
id: $scope.host.id,
|
||||
variables: $scope.extraVars === '---' || $scope.extraVars === '{}' ? null : $scope.extraVars,
|
||||
variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables,
|
||||
name: $scope.name,
|
||||
description: $scope.description,
|
||||
enabled: $scope.host.enabled
|
||||
@ -34,15 +34,14 @@
|
||||
};
|
||||
var init = function(){
|
||||
$scope.host = host;
|
||||
$scope.extraVars = host.variables === '' ? '---' : host.variables;
|
||||
generator.inject(form, {mode: 'edit', related: false, scope: $scope});
|
||||
$scope.extraVars = $scope.host.variables === '' ? '---' : $scope.host.variables;
|
||||
$scope.name = host.name;
|
||||
$scope.description = host.description;
|
||||
$scope.variables = host.variables === '' ? '---' : host.variables;
|
||||
ParseTypeChange({
|
||||
scope: $scope,
|
||||
field_id: 'host_variables',
|
||||
variable: 'extraVars',
|
||||
variable: 'variables',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
|
||||
export default
|
||||
['$scope', '$state', '$stateParams', 'PageRangeSetup', 'GetBasePath', 'DashboardHostsList',
|
||||
'generateList', 'PaginateInit', 'SetStatus', 'DashboardHostService', 'hosts',
|
||||
function($scope, $state, $stateParams, PageRangeSetup, GetBasePath, DashboardHostsList, GenerateList, PaginateInit, SetStatus, DashboardHostService, hosts){
|
||||
'generateList', 'PaginateInit', 'SetStatus', 'DashboardHostService', 'hosts', '$rootScope',
|
||||
function($scope, $state, $stateParams, PageRangeSetup, GetBasePath, DashboardHostsList, GenerateList, PaginateInit, SetStatus, DashboardHostService, hosts, $rootScope){
|
||||
var setJobStatus = function(){
|
||||
_.forEach($scope.hosts, function(value){
|
||||
SetStatus({
|
||||
@ -38,6 +38,20 @@ export default
|
||||
});
|
||||
setJobStatus();
|
||||
});
|
||||
var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) {
|
||||
if (toState.name === "dashboardHosts.edit") {
|
||||
$scope.rowBeingEdited = toParams.id;
|
||||
$scope.listBeingEdited = "hosts";
|
||||
}
|
||||
else {
|
||||
delete $scope.rowBeingEdited;
|
||||
delete $scope.listBeingEdited;
|
||||
}
|
||||
});
|
||||
// Remove the listener when the scope is destroyed to avoid a memory leak
|
||||
$scope.$on('$destroy', function() {
|
||||
cleanUpStateChangeListener();
|
||||
});
|
||||
var init = function(){
|
||||
$scope.list = list;
|
||||
$scope.host_active_search = false;
|
||||
@ -59,6 +73,10 @@ export default
|
||||
iterator: list.iterator
|
||||
});
|
||||
$scope.hostLoading = false;
|
||||
if($state.current.name === "dashboardHosts.edit") {
|
||||
$scope.rowBeingEdited = $state.params.id;
|
||||
$scope.listBeingEdited = "hosts";
|
||||
}
|
||||
};
|
||||
init();
|
||||
}];
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<table class="List-table">
|
||||
<tr class="List-tableHeaderRow">
|
||||
<th class="List-tableHeader DashboardList-tableHeader--name">
|
||||
Title
|
||||
Name
|
||||
</th>
|
||||
<th class="List-tableHeader DashboardList-tableHeader--activity">
|
||||
Activity
|
||||
@ -32,13 +32,15 @@
|
||||
<td class="DashboardList-activityCell">
|
||||
<aw-smart-status jobs="job_template.recent_jobs"></aw-smart-status>
|
||||
</td>
|
||||
<td class="List-actionButtonCell">
|
||||
<button class="List-actionButton" ng-click="launchJobTemplate(job_template.id)">
|
||||
<i class="icon-launch"></i>
|
||||
</button>
|
||||
<button class="List-actionButton" ng-click="editJobTemplate(job_template.id)">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<td class="List-actionsContainer">
|
||||
<div class="List-actionButtonCell">
|
||||
<button class="List-actionButton" ng-click="launchJobTemplate(job_template.id)">
|
||||
<i class="icon-launch"></i>
|
||||
</button>
|
||||
<button class="List-actionButton" ng-click="editJobTemplate(job_template.id)">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<div class="DashboardList-container">
|
||||
<table class="List-table">
|
||||
<tr>
|
||||
<th class="List-tableHeader DashboardList-tableHeader--name">Title</th>
|
||||
<th class="List-tableHeader DashboardList-tableHeader--name">Name</th>
|
||||
<th class="List-tableHeader DashboardList-tableHeader--time">Time</th>
|
||||
</tr>
|
||||
<tr class="List-tableRow"
|
||||
|
||||
@ -25,6 +25,7 @@ angular.module('JobTemplatesHelper', ['Utilities'])
|
||||
return function(params) {
|
||||
|
||||
var scope = params.scope,
|
||||
CredentialList = _.cloneDeep(CredentialList),
|
||||
defaultUrl = GetBasePath('job_templates'),
|
||||
// generator = GenerateForm,
|
||||
form = JobTemplateForm(),
|
||||
@ -162,6 +163,10 @@ angular.module('JobTemplatesHelper', ['Utilities'])
|
||||
input_type: "radio"
|
||||
});
|
||||
|
||||
CredentialList.basePath = GetBasePath('credentials') + '?kind=ssh';
|
||||
// remove "type" field from search options
|
||||
CredentialList.fields.kind.noSearch = true;
|
||||
|
||||
LookUpInit({
|
||||
url: GetBasePath('credentials') + '?kind=ssh',
|
||||
scope: scope,
|
||||
|
||||
@ -231,6 +231,12 @@ export default
|
||||
search_params = params.searchParams,
|
||||
spinner = (params.spinner === undefined) ? true : params.spinner, key;
|
||||
|
||||
var buildTooltips = function(data){
|
||||
data.forEach((val) => {
|
||||
val.status_tip = 'Job ' + val.status + ". Click for details.";
|
||||
});
|
||||
};
|
||||
|
||||
GenerateList.inject(list, {
|
||||
mode: 'edit',
|
||||
id: id,
|
||||
@ -260,6 +266,7 @@ export default
|
||||
scope.$on('PostRefresh', function(){
|
||||
JobsControllerInit({ scope: scope, parent_scope: parent_scope });
|
||||
JobsListUpdate({ scope: scope, parent_scope: parent_scope, list: list });
|
||||
buildTooltips(scope.jobs);
|
||||
parent_scope.$emit('listLoaded');
|
||||
});
|
||||
|
||||
@ -443,7 +450,7 @@ export default
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
id = params.id;
|
||||
InitiatePlaybookRun({ scope: scope, id: id });
|
||||
InitiatePlaybookRun({ scope: scope, id: id, relaunch: true });
|
||||
};
|
||||
}])
|
||||
|
||||
|
||||
@ -71,7 +71,7 @@ export default
|
||||
}
|
||||
catch (e) {
|
||||
Alert('Parse Error', 'Failed to parse valid YAML. ' + e.message);
|
||||
setTimeout( function() { scope.$apply( function() { scope[model] = 'yaml'; createField(); }); }, 500);
|
||||
setTimeout( function() { scope.$apply( function() { scope[model] = 'yaml'; createField(onReady, onChange, fld); }); }, 500);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@ -89,7 +89,7 @@ export default
|
||||
}
|
||||
catch (e) {
|
||||
Alert('Parse Error', 'Failed to parse valid JSON. ' + e.message);
|
||||
setTimeout( function() { scope.$apply( function() { scope[model] = 'json'; createField(); }); }, 500 );
|
||||
setTimeout( function() { scope.$apply( function() { scope[model] = 'json'; createField(onReady, onChange, fld); }); }, 500 );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -70,8 +70,11 @@ export default
|
||||
result = 'Success! Click for details';
|
||||
break;
|
||||
case 'failed':
|
||||
case 'missing':
|
||||
result = 'Failed. Click for details';
|
||||
break;
|
||||
case 'missing':
|
||||
result = 'Missing. Click for details';
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@ -197,7 +197,7 @@ function InventoriesList($scope, $rootScope, $location, $log,
|
||||
". Click for details\" aw-tip-placement=\"top\"><i class=\"fa icon-job-" + row.status + "\"></i></a></td>\n";
|
||||
html += "<td>" + ($filter('longDate')(row.finished)).replace(/ /,'<br />') + "</td>";
|
||||
html += "<td><a href=\"#/jobs/" + row.id + "\" " + "aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) +
|
||||
". Click for details\" aw-tip-placement=\"top\">" + ellipsis(row.name) + "</a></td>";
|
||||
". Click for details\" aw-tip-placement=\"top\">" + $filter('sanitize')(ellipsis(row.name)) + "</a></td>";
|
||||
html += "</tr>\n";
|
||||
});
|
||||
html += "</tbody>\n";
|
||||
@ -230,16 +230,16 @@ function InventoriesList($scope, $rootScope, $location, $log,
|
||||
data.results.forEach( function(row) {
|
||||
if (row.related.last_update) {
|
||||
html += "<tr>";
|
||||
html += "<td><a href=\"\" ng-click=\"viewJob('" + row.related.last_update + "')\" aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) + ". Click for details\" aw-tip-placement=\"top\"><i class=\"fa icon-job-" + row.status + "\"></i></a></td>";
|
||||
html += `<td><a href="" ng-click="viewJob('${row.related.last_update}')" aw-tool-tip="${row.status.charAt(0).toUpperCase() + row.status.slice(1)}. Click for details" aw-tip-placement="top"><i class="SmartStatus-tooltip--${row.status} fa icon-job-${row.status}"></i></a></td>`;
|
||||
html += "<td>" + ($filter('longDate')(row.last_updated)).replace(/ /,'<br />') + "</td>";
|
||||
html += "<td><a href=\"\" ng-click=\"viewJob('" + row.related.last_update + "')\">" + ellipsis(row.summary_fields.group.name) + "</a></td>";
|
||||
html += "<td><a href=\"\" ng-click=\"viewJob('" + row.related.last_update + "')\">" + $filter('sanitize')(ellipsis(row.summary_fields.group.name)) + "</a></td>";
|
||||
html += "</tr>\n";
|
||||
}
|
||||
else {
|
||||
html += "<tr>";
|
||||
html += "<td><a href=\"\" aw-tool-tip=\"No sync data\" aw-tip-placement=\"top\"><i class=\"fa icon-job-none\"></i></a></td>";
|
||||
html += "<td>NA</td>";
|
||||
html += "<td><a href=\"\">" + ellipsis(row.summary_fields.group.name) + "</a></td>";
|
||||
html += "<td><a href=\"\">" + $filter('sanitize')(ellipsis(row.summary_fields.group.name)) + "</a></td>";
|
||||
html += "</tr>\n";
|
||||
}
|
||||
});
|
||||
|
||||
@ -44,7 +44,7 @@ export default function() {
|
||||
autocomplete: false
|
||||
},
|
||||
limit: {
|
||||
label: 'Host Pattern',
|
||||
label: 'Limit',
|
||||
type: 'text',
|
||||
addRequired: false,
|
||||
awPopOver: '<p>The pattern used to target hosts in the ' +
|
||||
@ -54,7 +54,7 @@ export default function() {
|
||||
'<a id=\"adhoc_form_hostpatterns_doc_link\"' +
|
||||
'href=\"http://docs.ansible.com/intro_patterns.html\" ' +
|
||||
'target=\"_blank\">here</a>.</p>',
|
||||
dataTitle: 'Host Pattern',
|
||||
dataTitle: 'Limit',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: 'body'
|
||||
},
|
||||
@ -98,7 +98,7 @@ export default function() {
|
||||
addRequired: true,
|
||||
awPopOver:'<p>These are the verbosity levels for standard ' +
|
||||
'out of the command run that are supported.',
|
||||
dataTitle: 'Module',
|
||||
dataTitle: 'Verbosity',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: 'body',
|
||||
"default": 1
|
||||
|
||||
@ -12,6 +12,10 @@
|
||||
var generator = GenerateForm,
|
||||
form = GroupForm();
|
||||
|
||||
// remove "type" field from search options
|
||||
CredentialList = _.cloneDeep(CredentialList);
|
||||
CredentialList.fields.kind.noSearch = true;
|
||||
|
||||
$scope.formCancel = function(){
|
||||
$state.go('^');
|
||||
};
|
||||
@ -118,7 +122,7 @@
|
||||
$scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null;
|
||||
// azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint
|
||||
$scope.source_region_choices = source === 'azure_rm' ? $scope.azure_regions : $scope[source + '_regions'];
|
||||
$scope.cloudCredentialRequired = source !== 'manual' && source !== 'custom' ? true : false;
|
||||
$scope.cloudCredentialRequired = source !== '' && source !== 'custom' ? true : false;
|
||||
$scope.group_by = null;
|
||||
$scope.source_regions = null;
|
||||
$scope.credential = null;
|
||||
|
||||
@ -13,6 +13,11 @@
|
||||
GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData){
|
||||
var generator = GenerateForm,
|
||||
form = GroupForm();
|
||||
|
||||
// remove "type" field from search options
|
||||
CredentialList = _.cloneDeep(CredentialList);
|
||||
CredentialList.fields.kind.noSearch = true;
|
||||
|
||||
$scope.formCancel = function(){
|
||||
$state.go('^');
|
||||
};
|
||||
@ -117,7 +122,7 @@
|
||||
// reset fields
|
||||
// azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint
|
||||
$scope.source_region_choices = source.value === 'azure_rm' ? $scope.azure_regions : $scope[source.value + '_regions'];
|
||||
$scope.cloudCredentialRequired = source.value !== 'manual' && source.value !== 'custom' ? true : false;
|
||||
$scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' ? true : false;
|
||||
$scope.group_by = null;
|
||||
$scope.source_regions = null;
|
||||
$scope.credential = null;
|
||||
@ -247,7 +252,7 @@
|
||||
{inventory_script: inventorySourceData.source_script}
|
||||
);
|
||||
if (inventorySourceData.credential){
|
||||
GroupManageService.getCredential(inventorySourceData.credential).then(res => $scope.credential_name = res.data.name);
|
||||
$scope.credential_name = inventorySourceData.summary_fields.credential.name;
|
||||
}
|
||||
$scope = angular.extend($scope, groupData);
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ export default function(){
|
||||
icon: 'fa-edit',
|
||||
label: 'Edit',
|
||||
"class": 'btn-sm',
|
||||
awToolTip: 'Edit credential',
|
||||
awToolTip: 'Edit inventory script',
|
||||
dataPlacement: 'top'
|
||||
},
|
||||
"delete": {
|
||||
@ -63,7 +63,7 @@ export default function(){
|
||||
icon: 'fa-trash',
|
||||
label: 'Delete',
|
||||
"class": 'btn-sm',
|
||||
awToolTip: 'Delete credential',
|
||||
awToolTip: 'Delete inventory script',
|
||||
dataPlacement: 'top'
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,93 +18,49 @@
|
||||
|
||||
$scope.search = function(){
|
||||
Wait('start');
|
||||
if ($scope.searchStr === undefined){
|
||||
return;
|
||||
}
|
||||
//http://docs.ansible.com/ansible-tower/latest/html/towerapi/intro.html#filtering
|
||||
// SELECT WHERE host_name LIKE str OR WHERE play LIKE str OR WHERE task LIKE str AND host_name NOT ""
|
||||
// selecting non-empty host_name fields prevents us from displaying non-runner events, like playbook_on_task_start
|
||||
JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
or__host_name__icontains: $scope.searchStr,
|
||||
or__play__icontains: $scope.searchStr,
|
||||
or__task__icontains: $scope.searchStr,
|
||||
not__host_name: "" ,
|
||||
page_size: $scope.pageSize})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
var params = {
|
||||
host_name: $scope.hostName,
|
||||
};
|
||||
if ($scope.searchStr && $scope.searchStr !== ''){
|
||||
params.or__play__icontains = $scope.searchStr;
|
||||
params.or__task__icontains = $scope.searchStr;
|
||||
}
|
||||
|
||||
switch($scope.activeFilter){
|
||||
case 'skipped':
|
||||
params.event = 'runner_on_skipped';
|
||||
break;
|
||||
case 'unreachable':
|
||||
params.event = 'runner_on_unreachable';
|
||||
break;
|
||||
case 'ok':
|
||||
params.event = 'runner_on_ok';
|
||||
break;
|
||||
case 'failed':
|
||||
params.failed = true;
|
||||
break;
|
||||
case 'changed':
|
||||
params.changed = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
JobDetailService.getRelatedJobEvents($stateParams.id, params)
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.filters = ['all', 'changed', 'failed', 'ok', 'unreachable', 'skipped'];
|
||||
|
||||
var filter = function(filter){
|
||||
Wait('start');
|
||||
|
||||
if (filter === 'all'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
page_size: $scope.pageSize})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
// handle runner cases
|
||||
if (filter === 'skipped'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
event: 'runner_on_skipped'})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
if (filter === 'unreachable'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
event: 'runner_on_unreachable'})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
if (filter === 'ok'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
event__startswith: 'runner_on_ok'
|
||||
})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
// handle convience properties .changed .failed
|
||||
if (filter === 'changed'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
changed: true})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
if (filter === 'failed'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
failed: true,
|
||||
event__startswith: 'runner_on_failed'
|
||||
})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// watch select2 for changes
|
||||
$('.HostEvents-select').on("select2:select", function () {
|
||||
filter($('.HostEvents-select').val());
|
||||
$scope.activeFilter = $('.HostEvents-select').val();
|
||||
$scope.search();
|
||||
});
|
||||
|
||||
var init = function(){
|
||||
@ -116,12 +72,9 @@
|
||||
});
|
||||
// process the filter if one was passed
|
||||
if ($stateParams.filter){
|
||||
Wait('start');
|
||||
filter($stateParams.filter).success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
$('#HostEvents').modal('show');
|
||||
});
|
||||
$scope.activeFilter = $stateParams.filter;
|
||||
$scope.search();
|
||||
$('#HostEvents').modal('show');
|
||||
}
|
||||
else{
|
||||
$scope.results = hosts.data.results;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
|
||||
<div id="hosts-summary-section" class="section">
|
||||
<div class="JobDetail-instructions" ng-hide="hosts.length == 0 && !searchTerm"><span class="badge">4</span> Please select a host below to view a summary of all associated tasks.</div>
|
||||
<div class="JobDetail-searchHeaderRow" ng-hide="hosts.length == 0 && !searchTerm">
|
||||
<div class="JobDetail-instructions" ng-hide="hosts.length == 0 && !searchTerm && !searchActive"><span class="badge">4</span> Please select a host below to view a summary of all associated tasks.</div>
|
||||
<div class="JobDetail-searchHeaderRow" ng-hide="hosts.length == 0 && !searchTerm && !searchActive">
|
||||
<div class="JobDetail-searchContainer form-group">
|
||||
<div class="search-name">
|
||||
<form ng-submit="search()">
|
||||
@ -22,7 +22,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-header" ng-hide="hosts.length == 0 && !searchTerm">
|
||||
<div class="table-header" ng-hide="hosts.length == 0 && !searchTerm && !searchActive">
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@ -17,5 +17,15 @@ export default {
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
skip: true // Never display this state in breadcrumb.
|
||||
},
|
||||
onExit: function(){
|
||||
// close the job launch modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
// Destroy the dialog
|
||||
if($("#job-launch-modal").hasClass('ui-dialog-content')) {
|
||||
$('#job-launch-modal').dialog('destroy');
|
||||
}
|
||||
// Remove the directive from the page (if it's there)
|
||||
$('#content-container').find('submit-job').remove();
|
||||
}
|
||||
};
|
||||
|
||||
@ -13,13 +13,13 @@
|
||||
export default
|
||||
[ '$location', '$rootScope', '$filter', '$scope', '$compile', '$state', '$stateParams', '$log', 'ClearScope',
|
||||
'GetBasePath', 'Wait', 'ProcessErrors', 'SelectPlay', 'SelectTask', 'GetElapsed', 'JobIsFinished',
|
||||
'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'InitiatePlaybookRun', 'LoadPlays', 'LoadTasks',
|
||||
'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'RelaunchPlaybook', 'LoadPlays', 'LoadTasks',
|
||||
'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels', 'EditSchedule',
|
||||
'ParseTypeChange', 'JobDetailService',
|
||||
function(
|
||||
$location, $rootScope, $filter, $scope, $compile, $state, $stateParams, $log, ClearScope,
|
||||
GetBasePath, Wait, ProcessErrors, SelectPlay, SelectTask, GetElapsed, JobIsFinished,
|
||||
SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob, InitiatePlaybookRun, LoadPlays, LoadTasks,
|
||||
SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob, RelaunchPlaybook, LoadPlays, LoadTasks,
|
||||
ParseVariableString, GetChoices, fieldChoices, fieldLabels, EditSchedule,
|
||||
ParseTypeChange, JobDetailService
|
||||
) {
|
||||
@ -428,7 +428,7 @@ export default
|
||||
var params = {
|
||||
order_by: 'id'
|
||||
};
|
||||
if (scope.job.summary_fields.unified_job_template.unified_job_type === 'job'){
|
||||
|
||||
JobDetailService.getJobPlays(scope.job.id, params)
|
||||
.success( function(data) {
|
||||
scope.next_plays = data.next;
|
||||
@ -514,7 +514,6 @@ export default
|
||||
}
|
||||
scope.$emit('LoadTasks', events_url);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -841,7 +840,7 @@ export default
|
||||
};
|
||||
|
||||
scope.searchTasks = function() {
|
||||
var params;
|
||||
var params = {};
|
||||
if (scope.search_task_name) {
|
||||
scope.searchTasksEnabled = false;
|
||||
}
|
||||
@ -866,7 +865,7 @@ export default
|
||||
};
|
||||
|
||||
scope.searchHosts = function() {
|
||||
var params;
|
||||
var params = {};
|
||||
if (scope.search_host_name) {
|
||||
scope.searchHostsEnabled = false;
|
||||
}
|
||||
@ -920,7 +919,7 @@ export default
|
||||
};
|
||||
|
||||
scope.relaunchJob = function() {
|
||||
InitiatePlaybookRun({
|
||||
RelaunchPlaybook({
|
||||
scope: scope,
|
||||
id: scope.job.id
|
||||
});
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="JobDetail-actions">
|
||||
<button id="relaunch-job-button" class="List-actionButton JobDetail-launchButton" data-placement="top" mode="all" ng-click="relaunchJob()" aw-tool-tip="Relaunch using the same parameters" data-original-title="" title=""><i class="fa fa-rocket"></i> </button>
|
||||
<button id="relaunch-job-button" class="List-actionButton JobDetail-launchButton" data-placement="top" mode="all" ng-click="relaunchJob()" aw-tool-tip="Relaunch using the same parameters" data-original-title="" title=""><i class="icon-launch"></i> </button>
|
||||
<button id="cancel-job-button" class="List-actionButton List-actionButton--delete JobDetail-launchButton" data-placement="top" ng-click="deleteJob()" ng-show="job_status.status == 'running' || job_status.status=='pending' " aw-tool-tip="Cancel" data-original-title="" title=""><i class="fa fa-minus-circle"></i> </button>
|
||||
<button id="delete-job-button" class="List-actionButton List-actionButton--delete JobDetail-launchButton" data-placement="top" ng-click="deleteJob()" ng-hide="job_status.status == 'running' || job_status.status == 'pending' " aw-tool-tip="Delete" data-original-title="" title=""><i class="fa fa-trash-o"></i> </button>
|
||||
</div>
|
||||
@ -164,7 +164,7 @@
|
||||
<!--- end of results-->
|
||||
|
||||
<!--beginning of details-->
|
||||
<div id="job-detail-panel" class="JobDetail-resultsContainer Panel" ng-show="!stdoutFullScreen && job.summary_fields.unified_job_template.unified_job_type == 'job'">
|
||||
<div id="job-detail-panel" class="JobDetail-resultsContainer Panel" ng-show="!stdoutFullScreen">
|
||||
<div class="JobDetail-panelHeader">
|
||||
<div class="JobDetail-expandContainer">
|
||||
<a class="JobDetail-panelHeaderText" ng-show="lessDetail" href="" ng-click="toggleLessDetail()">
|
||||
@ -354,7 +354,7 @@
|
||||
<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">
|
||||
<a ui-sref="jobDetail.host-event.details({eventId: result.id, taskId: selectedTask})" aw-tool-tip="Event ID: {{ result.id }}<br \>Status: {{ result.status_text }}. Click for details" data-placement="top"><i ng-show="result.status_text != 'Unreachable'" class="JobDetail-statusIcon fa icon-job-{{ result.status }}"></i><span ng-show="result.status_text != 'Unreachable'">{{ result.name }}</span><i ng-show="result.status_text == 'Unreachable'" class="JobDetail-statusIcon fa icon-job-unreachable"></i><span ng-show="result.status_text == 'Unreachable'">{{ result.name }}</span></a>
|
||||
<a ui-sref="jobDetail.host-event.details({eventId: result.id, taskId: selectedTask})" aw-tool-tip="Event ID: {{ result.id }}<br \>Status: {{ result.status_text }}. Click for details" data-tip-watch="result.tip" data-placement="top"><i ng-show="result.status_text != 'Unreachable'" class="JobDetail-statusIcon fa icon-job-{{ result.status }}"></i><span ng-show="result.status_text != 'Unreachable'">{{ result.name }}</span><i ng-show="result.status_text == 'Unreachable'" class="JobDetail-statusIcon fa icon-job-unreachable"></i><span ng-show="result.status_text == 'Unreachable'">{{ result.name }}</span></a>
|
||||
</td>
|
||||
<td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3 item-column">{{ result.item }}</td>
|
||||
<td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3">{{ result.msg }}</td>
|
||||
@ -379,7 +379,7 @@
|
||||
<!--end of details-->
|
||||
|
||||
<!--beginning of events summary-->
|
||||
<div id="events-summary-panel" class="JobDetail-resultsContainer Panel" ng-show="!stdoutFullScreen && job.summary_fields.unified_job_template.unified_job_type == 'job'">
|
||||
<div id="events-summary-panel" class="JobDetail-resultsContainer Panel" ng-show="!stdoutFullScreen">
|
||||
<div class="JobDetail-panelHeader">
|
||||
<div class="JobDetail-expandContainer">
|
||||
<a class="JobDetail-panelHeaderText" ng-show="lessEvents" ui-sref="jobDetail.host-summary" ng-click="toggleLessEvents()">
|
||||
|
||||
@ -30,5 +30,15 @@ export default {
|
||||
}]
|
||||
},
|
||||
templateUrl: templateUrl('job-detail/job-detail'),
|
||||
controller: 'JobDetailController'
|
||||
controller: 'JobDetailController',
|
||||
onExit: function(){
|
||||
// close the job launch modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
// Destroy the dialog
|
||||
if($("#job-launch-modal").hasClass('ui-dialog-content')) {
|
||||
$('#job-launch-modal').dialog('destroy');
|
||||
}
|
||||
// Remove the directive from the page (if it's there)
|
||||
$('#content-container').find('submit-job').remove();
|
||||
}
|
||||
};
|
||||
|
||||
@ -39,6 +39,13 @@ export default
|
||||
}
|
||||
catch(err){return;}
|
||||
},
|
||||
processsEventTip: function(event, status){
|
||||
try{
|
||||
var string = `Event ID: ${ event.id }<br>Status: ${ _.capitalize(status.status)}. Click for details`;
|
||||
return typeof item === 'object' ? JSON.stringify(string) : string;
|
||||
}
|
||||
catch(err){return;}
|
||||
},
|
||||
// Generate a helper class for job_event statuses
|
||||
// the stack for which status to display is
|
||||
// unreachable > failed > changed > ok
|
||||
@ -88,6 +95,7 @@ export default
|
||||
var status = self.processEventStatus(event);
|
||||
var msg = self.processEventMsg(event);
|
||||
var item = self.processEventItem(event);
|
||||
var tip = self.processsEventTip(event, status);
|
||||
results.push({
|
||||
id: event.id,
|
||||
status: status.status,
|
||||
@ -96,6 +104,7 @@ export default
|
||||
task_id: event.parent,
|
||||
name: event.event_data.host,
|
||||
created: event.created,
|
||||
tip: typeof tip === 'undefined' ? undefined : tip,
|
||||
msg: typeof msg === 'undefined' ? undefined : msg,
|
||||
item: typeof item === 'undefined' ? undefined : item
|
||||
});
|
||||
|
||||
@ -9,10 +9,11 @@ export default
|
||||
return function (params) {
|
||||
var scope = params.scope.$new(),
|
||||
id = params.id,
|
||||
relaunch = params.relaunch || false,
|
||||
system_job = params.system_job || false;
|
||||
scope.job_template_id = id;
|
||||
|
||||
var el = $compile( "<submit-job data-submit-job-id=" + id + " data-submit-job-system=" + system_job + "></submit-job>" )( scope );
|
||||
var el = $compile( "<submit-job data-submit-job-id=" + id + " data-submit-job-system=" + system_job + " data-submit-job-relaunch=" + relaunch + "></submit-job>" )( scope );
|
||||
$('#content-container').remove('submit-job').append( el );
|
||||
};
|
||||
}
|
||||
|
||||
@ -103,8 +103,8 @@
|
||||
}
|
||||
.JobSubmission-step--active {
|
||||
color: @default-bg!important;
|
||||
background-color: @d7grey!important;
|
||||
border-color: @d7grey!important;
|
||||
background-color: @default-icon!important;
|
||||
border-color: @default-icon!important;
|
||||
cursor: default!important;
|
||||
}
|
||||
.JobSubmission-step--disabled {
|
||||
|
||||
@ -128,14 +128,14 @@ export default
|
||||
// This gets things started - goes out and hits the launch endpoint (based on launch/relaunch) and
|
||||
// prepares the form fields, defauts, etc.
|
||||
$scope.init = function() {
|
||||
|
||||
$scope.forms = {};
|
||||
$scope.passwords = {};
|
||||
|
||||
var base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
isRelaunch = !(base === 'job_templates' || base === 'portal' || base === 'inventories' || base === 'home');
|
||||
// As of 3.0, the only place the user can relaunch a
|
||||
// playbook is on jobTemplates.edit (completed_jobs tab),
|
||||
// jobs, and jobDetails $states.
|
||||
|
||||
if (!isRelaunch) {
|
||||
if (!$scope.submitJobRelaunch) {
|
||||
launch_url = GetBasePath('job_templates') + $scope.submitJobId + '/launch/';
|
||||
}
|
||||
else {
|
||||
@ -187,7 +187,7 @@ export default
|
||||
updateRequiredPasswords();
|
||||
}
|
||||
|
||||
if( (isRelaunch && !$scope.password_needed) || (!isRelaunch && $scope.can_start_without_user_input && !$scope.ask_inventory_on_launch && !$scope.ask_credential_on_launch && !$scope.has_other_prompts && !$scope.survey_enabled)) {
|
||||
if( ($scope.submitJobRelaunch && !$scope.password_needed) || (!$scope.submitJobRelaunch && $scope.can_start_without_user_input && !$scope.ask_inventory_on_launch && !$scope.ask_credential_on_launch && !$scope.has_other_prompts && !$scope.survey_enabled)) {
|
||||
// The job can be launched if
|
||||
// a) It's a relaunch and no passwords are needed
|
||||
// or
|
||||
@ -215,7 +215,7 @@ export default
|
||||
$scope.openLaunchModal();
|
||||
};
|
||||
|
||||
if(isRelaunch) {
|
||||
if($scope.submitJobRelaunch) {
|
||||
// Go out and get some of the job details like inv, cred, name
|
||||
Rest.setUrl(GetBasePath('jobs') + $scope.submitJobId);
|
||||
Rest.get()
|
||||
@ -331,6 +331,7 @@ export default
|
||||
var credential_url = GetBasePath('credentials') + '?kind=ssh';
|
||||
|
||||
var credList = _.cloneDeep(CredentialList);
|
||||
credList.basePath = GetBasePath('credentials') + '?kind=ssh';
|
||||
credList.fields.description.searchable = false;
|
||||
credList.fields.kind.searchable = false;
|
||||
|
||||
@ -536,8 +537,9 @@ export default
|
||||
// It shares the same scope with this directive and will
|
||||
// pull the new value of parseType out to determine which
|
||||
// direction to convert the extra vars
|
||||
|
||||
$scope.parseType = $scope.other_prompt_data.parseType;
|
||||
$scope.parseTypeChange();
|
||||
$scope.parseTypeChange('parseType', 'jobLaunchVariables');
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -11,7 +11,8 @@ export default [ 'templateUrl', 'CreateDialog', 'Wait', 'CreateSelect2', 'ParseT
|
||||
return {
|
||||
scope: {
|
||||
submitJobId: '=',
|
||||
submitJobSystem: '='
|
||||
submitJobSystem: '=',
|
||||
submitJobRelaunch: '='
|
||||
},
|
||||
templateUrl: templateUrl('job-submission/job-submission'),
|
||||
controller: jobSubmissionController,
|
||||
|
||||
@ -34,6 +34,9 @@
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
context = (base === 'job_templates') ? 'job_template' : 'inv';
|
||||
|
||||
// remove "type" field from search options
|
||||
CredentialList.fields.kind.noSearch = true;
|
||||
|
||||
CallbackHelpInit({ scope: $scope });
|
||||
$scope.can_edit = true;
|
||||
generator.inject(form, { mode: 'add', related: false, scope: $scope });
|
||||
@ -82,6 +85,7 @@
|
||||
NetworkCredentialList.iterator = 'networkcredential';
|
||||
NetworkCredentialList.basePath = '/api/v1/credentials?kind=net';
|
||||
|
||||
|
||||
SurveyControllerInit({
|
||||
scope: $scope,
|
||||
parent_scope: $scope
|
||||
@ -375,7 +379,7 @@
|
||||
</string>
|
||||
</p>
|
||||
</div>`,
|
||||
'alert-info', saveCompleted, null, null,
|
||||
'alert-danger', saveCompleted, null, null,
|
||||
null, true);
|
||||
}
|
||||
var orgDefer = $q.defer();
|
||||
|
||||
@ -14,5 +14,19 @@ export default {
|
||||
ncyBreadcrumb: {
|
||||
parent: "jobTemplates",
|
||||
label: "CREATE JOB TEMPLATE"
|
||||
},
|
||||
onExit: function(){
|
||||
// close the job launch modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
// Destroy the dialog
|
||||
if($("#job-launch-modal").hasClass('ui-dialog-content')) {
|
||||
$('#job-launch-modal').dialog('destroy');
|
||||
}
|
||||
// Remove the directive from the page (if it's there)
|
||||
$('#content-container').find('submit-job').remove();
|
||||
|
||||
if($("#survey-modal-dialog").hasClass('ui-dialog-content')) {
|
||||
$('#survey-modal-dialog').dialog('destroy');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -46,6 +46,9 @@ export default
|
||||
checkSCMStatus, getPlaybooks, callback,
|
||||
choicesCount = 0;
|
||||
|
||||
// remove "type" field from search options
|
||||
CredentialList = _.cloneDeep(CredentialList);
|
||||
CredentialList.fields.kind.noSearch = true;
|
||||
|
||||
CallbackHelpInit({ scope: $scope });
|
||||
|
||||
@ -347,20 +350,13 @@ export default
|
||||
|
||||
ParseTypeChange({ scope: $scope, field_id: 'job_templates_variables', onChange: callback });
|
||||
|
||||
if (related_cloud_credential) {
|
||||
Rest.setUrl(related_cloud_credential);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
$scope.$emit('cloudCredentialReady', data.name);
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, null, {hdr: 'Error!',
|
||||
msg: 'Failed to related cloud credential. GET returned status: ' + status });
|
||||
});
|
||||
if($scope.job_template_obj.summary_fields.cloud_credential && related_cloud_credential) {
|
||||
$scope.$emit('cloudCredentialReady', $scope.job_template_obj.summary_fields.cloud_credential.name);
|
||||
} else {
|
||||
// No existing cloud credential
|
||||
$scope.$emit('cloudCredentialReady', null);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Wait('start');
|
||||
@ -446,31 +442,40 @@ export default
|
||||
}
|
||||
});
|
||||
};
|
||||
if($state.params.id !== "null"){
|
||||
Rest.setUrl(defaultUrl + $state.params.id +
|
||||
"/labels");
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
if (data.next) {
|
||||
getNext(data, data.results, seeMoreResolve);
|
||||
} else {
|
||||
seeMoreResolve.resolve(data.results);
|
||||
}
|
||||
|
||||
Rest.setUrl(defaultUrl + $state.params.id +
|
||||
"/labels");
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
if (data.next) {
|
||||
getNext(data, data.results, seeMoreResolve);
|
||||
} else {
|
||||
seeMoreResolve.resolve(data.results);
|
||||
}
|
||||
|
||||
seeMoreResolve.promise.then(function (labels) {
|
||||
$scope.$emit("choicesReady");
|
||||
var opts = labels
|
||||
.map(i => ({id: i.id + "",
|
||||
test: i.name}));
|
||||
CreateSelect2({
|
||||
element:'#job_templates_labels',
|
||||
multiple: true,
|
||||
addNew: true,
|
||||
opts: opts
|
||||
seeMoreResolve.promise.then(function (labels) {
|
||||
$scope.$emit("choicesReady");
|
||||
var opts = labels
|
||||
.map(i => ({id: i.id + "",
|
||||
test: i.name}));
|
||||
CreateSelect2({
|
||||
element:'#job_templates_labels',
|
||||
multiple: true,
|
||||
addNew: true,
|
||||
opts: opts
|
||||
});
|
||||
Wait("stop");
|
||||
});
|
||||
Wait("stop");
|
||||
}).error(function(){
|
||||
// job template id is null in this case
|
||||
$scope.$emit("choicesReady");
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
// job template doesn't exist
|
||||
$scope.$emit("choicesReady");
|
||||
}
|
||||
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, form, {
|
||||
|
||||
@ -17,5 +17,19 @@ export default {
|
||||
ncyBreadcrumb: {
|
||||
parent: 'jobTemplates',
|
||||
label: "{{name}}"
|
||||
},
|
||||
onExit: function(){
|
||||
// close the job launch modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
// Destroy the dialog
|
||||
if($("#job-launch-modal").hasClass('ui-dialog-content')) {
|
||||
$('#job-launch-modal').dialog('destroy');
|
||||
}
|
||||
// Remove the directive from the page (if it's there)
|
||||
$('#content-container').find('submit-job').remove();
|
||||
|
||||
if($("#survey-modal-dialog").hasClass('ui-dialog-content')) {
|
||||
$('#survey-modal-dialog').dialog('destroy');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -74,13 +74,17 @@ export default
|
||||
});
|
||||
};
|
||||
|
||||
scope.$watch(scope.$parent.list.iterator+".summary_fields.labels.results", function() {
|
||||
scope.$watchCollection(scope.$parent.list.iterator, function() {
|
||||
// To keep the array of labels fresh, we need to set up a watcher - otherwise, the
|
||||
// array will get set initially and then never be updated as labels are removed
|
||||
if (scope[scope.$parent.list.iterator].summary_fields.labels){
|
||||
scope.labels = scope[scope.$parent.list.iterator].summary_fields.labels.results;
|
||||
scope.count = scope[scope.$parent.list.iterator].summary_fields.labels.count;
|
||||
}
|
||||
else{
|
||||
scope.labels = null;
|
||||
scope.count = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -17,5 +17,15 @@ export default {
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: "JOB TEMPLATES"
|
||||
},
|
||||
onExit: function(){
|
||||
// close the job launch modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
// Destroy the dialog
|
||||
if($("#job-launch-modal").hasClass('ui-dialog-content')) {
|
||||
$('#job-launch-modal').dialog('destroy');
|
||||
}
|
||||
// Remove the directive from the page (if it's there)
|
||||
$('#content-container').find('submit-job').remove();
|
||||
}
|
||||
};
|
||||
|
||||
@ -21,7 +21,8 @@ export default
|
||||
label: '',
|
||||
searchLabel: 'Status',
|
||||
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus',
|
||||
awToolTip: "Job {{ all_job.status }}. Click for details.",
|
||||
dataTipWatch: 'all_job.status_tip',
|
||||
awToolTip: "{{ all_job.status_tip }}",
|
||||
awTipPlacement: "right",
|
||||
dataTitle: "{{ all_job.status_popover_title }}",
|
||||
icon: 'icon-job-{{ all_job.status }}',
|
||||
@ -48,6 +49,7 @@ export default
|
||||
ngClick: "viewJobDetails(all_job)",
|
||||
defaultSearchField: true,
|
||||
awToolTip: "{{ all_job.name | sanitize }}",
|
||||
dataTipWatch: 'all_job.name',
|
||||
dataPlacement: 'top'
|
||||
},
|
||||
type: {
|
||||
|
||||
@ -45,6 +45,7 @@ export default
|
||||
owners: {
|
||||
label: 'Owners',
|
||||
type: 'owners',
|
||||
searchable: false,
|
||||
nosort: true,
|
||||
excludeModal: true,
|
||||
columnClass: 'col-md-2 hidden-sm hidden-xs'
|
||||
|
||||
@ -54,6 +54,10 @@ export default
|
||||
label: 'Disabled?',
|
||||
searchSingleValue: true,
|
||||
searchType: 'boolean',
|
||||
typeOptions: [
|
||||
{label: "Yes", value: false},
|
||||
{label: "No", value: true}
|
||||
],
|
||||
searchValue: 'false',
|
||||
searchOnly: true
|
||||
},
|
||||
|
||||
@ -43,6 +43,7 @@ export default
|
||||
searchable: false,
|
||||
filter: "longDate",
|
||||
key: true,
|
||||
desc: true,
|
||||
columnClass: "col-lg-4 col-md-4 col-sm-3"
|
||||
}
|
||||
},
|
||||
|
||||
@ -35,6 +35,7 @@ export default
|
||||
sourceField: 'name',
|
||||
ngClick: "editSchedule(schedule)",
|
||||
awToolTip: "{{ schedule.nameTip | sanitize}}",
|
||||
dataTipWatch: 'schedule.nameTip',
|
||||
dataPlacement: "top",
|
||||
defaultSearchField: true
|
||||
},
|
||||
@ -45,14 +46,17 @@ export default
|
||||
sourceModel: 'unified_job_template',
|
||||
sourceField: 'unified_job_type',
|
||||
ngBind: 'schedule.type_label',
|
||||
searchField: 'unified_job_template__polymorphic_ctype__name',
|
||||
searchLable: 'Type',
|
||||
searchField: 'unified_job_template__polymorphic_ctype__model',
|
||||
filterBySearchField: true,
|
||||
searchLabel: 'Type',
|
||||
searchable: true,
|
||||
searchType: 'select',
|
||||
searchOptions: [
|
||||
{ value: 'inventory source', label: 'Inventory Sync' },
|
||||
{ value: 'job template', label: 'Playbook Run' },
|
||||
{ value: 'project', label: 'SCM Update' }
|
||||
{ value: 'inventorysource', label: 'Inventory Sync' },
|
||||
{ value: 'jobtemplate', label: 'Playbook Run' },
|
||||
{ value: 'project', label: 'SCM Update' },
|
||||
{ value: 'systemjobtemplate', label: 'Management Job'}
|
||||
|
||||
]
|
||||
},
|
||||
next_run: {
|
||||
|
||||
@ -137,6 +137,7 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope',
|
||||
$rootScope.sessionTimer = timer;
|
||||
$rootScope.$emit('OpenSocket');
|
||||
$rootScope.user_is_superuser = data.results[0].is_superuser;
|
||||
$rootScope.user_is_system_auditor = data.results[0].is_system_auditor;
|
||||
scope.$emit('AuthorizationGetLicense');
|
||||
});
|
||||
})
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<button class="MgmtCards-actionItem List-actionButton"
|
||||
ng-click='chooseRunJob(card.id, card.name)'
|
||||
data-placement="top" aw-tool-tip="Launch Management Job" data-original-title="" title="">
|
||||
<i class="MgmtCards-actionItemIcon fa fa-rocket"></i>
|
||||
<i class="MgmtCards-actionItemIcon icon-launch"></i>
|
||||
</button>
|
||||
<button class="MgmtCards-actionItem List-actionButton"
|
||||
ng-click='configureSchedule(card.id)'
|
||||
|
||||
@ -131,7 +131,7 @@ export default
|
||||
|
||||
recent_notifications.forEach(function(row) {
|
||||
html += "<tr>\n";
|
||||
html += "<td><i class=\"fa icon-job-" + row.status + "\"></i></td>\n";
|
||||
html += `<td><i class=\"SmartStatus-tooltip--${row.status} fa icon-job-${row.status}"></i></td>`;
|
||||
html += "<td>" + ($filter('longDate')(row.created)).replace(/ /,'<br />') + "</td>\n";
|
||||
html += "</tr>\n";
|
||||
});
|
||||
@ -147,7 +147,9 @@ export default
|
||||
};
|
||||
|
||||
scope.testNotification = function(){
|
||||
var name = $filter('sanitize')(this.notification_template.name);
|
||||
var name = $filter('sanitize')(this.notification_template.name),
|
||||
pending_retries = 10;
|
||||
|
||||
Rest.setUrl(defaultUrl + this.notification_template.id +'/test/');
|
||||
Rest.post({})
|
||||
.then(function (data) {
|
||||
@ -156,31 +158,7 @@ export default
|
||||
// Using a setTimeout here to wait for the
|
||||
// notification to be processed and for a status
|
||||
// to be returned from the API.
|
||||
setTimeout(function(){
|
||||
var id = data.data.notification,
|
||||
url = GetBasePath('notifications') + id;
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.then(function (res) {
|
||||
Wait('stop');
|
||||
if(res && res.data && res.data.status && res.data.status === "successful"){
|
||||
scope.search(list.iterator);
|
||||
ngToast.success({
|
||||
content: `<i class="fa fa-check-circle Toast-successIcon"></i> <b>${name}:</b> Notification sent.`
|
||||
});
|
||||
}
|
||||
else if(res && res.data && res.data.status && res.data.status === "failed"){
|
||||
scope.search(list.iterator);
|
||||
ngToast.danger({
|
||||
content: `<i class="fa fa-exclamation-triangle Toast-successIcon"></i> <b>${name}:</b> Notification failed.`
|
||||
});
|
||||
}
|
||||
else {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + url + ' failed. Notification returned status: ' + status });
|
||||
}
|
||||
});
|
||||
} , 5000);
|
||||
retrieveStatus(data.data.notification);
|
||||
}
|
||||
else {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
@ -192,6 +170,40 @@ export default
|
||||
content: `<i class="fa fa-check-circle Toast-successIcon"></i> <b>${name}:</b> Notification Failed.`,
|
||||
});
|
||||
});
|
||||
|
||||
function retrieveStatus(id){
|
||||
setTimeout(function(){
|
||||
var url = GetBasePath('notifications') + id;
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.then(function (res) {
|
||||
if(res && res.data && res.data.status && res.data.status === "successful"){
|
||||
scope.search(list.iterator);
|
||||
ngToast.success({
|
||||
content: `<i class="fa fa-check-circle Toast-successIcon"></i> <b>${name}:</b> Notification sent.`
|
||||
});
|
||||
Wait('stop');
|
||||
}
|
||||
else if(res && res.data && res.data.status && res.data.status === "failed"){
|
||||
scope.search(list.iterator);
|
||||
ngToast.danger({
|
||||
content: `<i class="fa fa-exclamation-triangle Toast-successIcon"></i> <b>${name}:</b> Notification failed.`
|
||||
});
|
||||
Wait('stop');
|
||||
}
|
||||
else if(res && res.data && res.data.status && res.data.status === "pending" && pending_retries>0){
|
||||
pending_retries--;
|
||||
retrieveStatus(id);
|
||||
}
|
||||
else {
|
||||
Wait('stop');
|
||||
ProcessErrors(scope, null, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to test notifications failed.' });
|
||||
}
|
||||
|
||||
});
|
||||
} , 5000);
|
||||
}
|
||||
};
|
||||
|
||||
scope.addNotification = function(){
|
||||
|
||||
@ -121,6 +121,16 @@ export default [
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
},
|
||||
onExit: function(){
|
||||
// close the job launch modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
// Destroy the dialog
|
||||
if($("#job-launch-modal").hasClass('ui-dialog-content')) {
|
||||
$('#job-launch-modal').dialog('destroy');
|
||||
}
|
||||
// Remove the directive from the page (if it's there)
|
||||
$('#content-container').find('submit-job').remove();
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
export function PortalModeJobsController($scope, $rootScope, GetBasePath, GenerateList, PortalJobsList, SearchInit,
|
||||
PaginateInit){
|
||||
|
||||
var list = PortalJobsList,
|
||||
var list = _.cloneDeep(PortalJobsList),
|
||||
view = GenerateList,
|
||||
// show user jobs by default
|
||||
defaultUrl = GetBasePath('jobs') + '?created_by=' + $rootScope.current_user.id,
|
||||
@ -23,7 +23,13 @@ export function PortalModeJobsController($scope, $rootScope, GetBasePath, Genera
|
||||
$scope.iterator = list.iterator;
|
||||
$scope.activeFilter = 'user';
|
||||
|
||||
var init = function(sort){
|
||||
var init = function(url){
|
||||
defaultUrl = url ? url : defaultUrl;
|
||||
// We need to explicitly set the lists base path so that tag searching will keep the '?created_by'
|
||||
// query param when it's present. If we don't do this, then tag search will just grab the base
|
||||
// path for this list (/api/v1/jobs) and lose the created_by filter
|
||||
list.basePath = defaultUrl;
|
||||
|
||||
view.inject(list, {
|
||||
id: 'portal-jobs',
|
||||
mode: 'edit',
|
||||
@ -44,32 +50,26 @@ export function PortalModeJobsController($scope, $rootScope, GetBasePath, Genera
|
||||
url: defaultUrl,
|
||||
pageSize: pageSize
|
||||
});
|
||||
$scope.search (list.iterator);
|
||||
if(sort) {
|
||||
// hack to default to descending sort order
|
||||
$scope.sort('job','finished');
|
||||
}
|
||||
|
||||
$scope.search(list.iterator);
|
||||
};
|
||||
|
||||
|
||||
$scope.filterUser = function(){
|
||||
$scope.activeFilter = 'user';
|
||||
defaultUrl = GetBasePath('jobs') + '?created_by=' + $rootScope.current_user.id;
|
||||
init();
|
||||
init(defaultUrl);
|
||||
};
|
||||
|
||||
$scope.filterAll = function(){
|
||||
$scope.activeFilter = 'all';
|
||||
defaultUrl = GetBasePath('jobs');
|
||||
init();
|
||||
init(defaultUrl);
|
||||
};
|
||||
|
||||
$scope.refresh = function(){
|
||||
$scope.search(list.iterator);
|
||||
};
|
||||
|
||||
init(true);
|
||||
init();
|
||||
}
|
||||
|
||||
PortalModeJobsController.$inject = ['$scope', '$rootScope', 'GetBasePath', 'generateList', 'PortalJobsList', 'SearchInit',
|
||||
|
||||
@ -24,5 +24,15 @@ export default {
|
||||
templateUrl: templateUrl('portal-mode/portal-mode-jobs'),
|
||||
controller: PortalModeJobsController
|
||||
}
|
||||
},
|
||||
onExit: function(){
|
||||
// close the job launch modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
// Destroy the dialog
|
||||
if($("#job-launch-modal").hasClass('ui-dialog-content')) {
|
||||
$('#job-launch-modal').dialog('destroy');
|
||||
}
|
||||
// Remove the directive from the page (if it's there)
|
||||
$('#content-container').find('submit-job').remove();
|
||||
}
|
||||
};
|
||||
|
||||
@ -78,11 +78,11 @@ export default ['$compile', '$filter', '$state', '$stateParams', 'AddSchedule',
|
||||
// extra_data field is not manifested in the UI when scheduling a Management Job
|
||||
if ($state.current.name === 'jobTemplateSchedules.add'){
|
||||
$scope.parseType = 'yaml';
|
||||
// grab any existing extra_vars from parent job_template
|
||||
var defaultUrl = GetBasePath('job_templates') + $stateParams.id + '/';
|
||||
Rest.setUrl(defaultUrl);
|
||||
Rest.get().then(function(res){
|
||||
// sanitize
|
||||
var data = JSON.parse(JSON.stringify(res.data.extra_vars));
|
||||
var data = res.data.extra_vars;
|
||||
$scope.extraVars = data === '' ? '---' : data;
|
||||
ParseTypeChange({
|
||||
scope: $scope,
|
||||
|
||||
@ -36,6 +36,7 @@ export default
|
||||
mustUpdateValue = false;
|
||||
scope.dateValueMoment = moment(newValue, ['MM/DD/YYYY'], moment.locale());
|
||||
scope.dateValue = scope.dateValueMoment.format('L');
|
||||
element.find(".DatePicker").systemTrackingDP('update', scope.dateValue);
|
||||
}
|
||||
}, true);
|
||||
|
||||
@ -54,7 +55,7 @@ export default
|
||||
|
||||
element.find(".DatePicker").addClass("input-prepend date");
|
||||
element.find(".DatePicker").find(".DatePicker-icon").addClass("add-on");
|
||||
$(".date").systemTrackingDP({
|
||||
element.find(".DatePicker").systemTrackingDP({
|
||||
autoclose: true,
|
||||
language: localeKey,
|
||||
format: dateFormat
|
||||
|
||||
@ -1,4 +1,20 @@
|
||||
export default ['$compile', '$state', '$stateParams', 'EditSchedule', 'Wait', '$scope', '$rootScope', 'CreateSelect2', 'ParseTypeChange', function($compile, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope, CreateSelect2, ParseTypeChange) {
|
||||
export default ['$filter', '$compile', '$state', '$stateParams', 'EditSchedule', 'Wait', '$scope', '$rootScope', 'CreateSelect2', 'ParseTypeChange',
|
||||
function($filter, $compile, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope, CreateSelect2, ParseTypeChange) {
|
||||
|
||||
$scope.processSchedulerEndDt = function(){
|
||||
// set the schedulerEndDt to be equal to schedulerStartDt + 1 day @ midnight
|
||||
var dt = new Date($scope.schedulerUTCTime);
|
||||
// increment date by 1 day
|
||||
dt.setDate(dt.getDate() + 1);
|
||||
var month = $filter('schZeroPad')(dt.getMonth() + 1, 2),
|
||||
day = $filter('schZeroPad')(dt.getDate(), 2);
|
||||
$scope.$parent.schedulerEndDt = month + '/' + day + '/' + dt.getFullYear();
|
||||
};
|
||||
// initial end @ midnight values
|
||||
$scope.schedulerEndHour = "00";
|
||||
$scope.schedulerEndMinute = "00";
|
||||
$scope.schedulerEndSecond = "00";
|
||||
|
||||
$scope.$on("ScheduleFormCreated", function(e, scope) {
|
||||
$scope.hideForm = false;
|
||||
$scope = angular.extend($scope, scope);
|
||||
|
||||
@ -630,8 +630,8 @@
|
||||
</a>
|
||||
|
||||
<div class="parse-selection">
|
||||
<input type="radio" ng-model="parseType" ng-change="parseTypeChange()" value="yaml"><span class="parse-label">YAML</span>
|
||||
<input type="radio" ng-model="parseType" ng-change="parseTypeChange()" value="json"> <span class="parse-label">JSON</span>
|
||||
<input type="radio" ng-model="parseType" ng-change="parseTypeChange('parseType', 'extraVars')" value="yaml"><span class="parse-label">YAML</span>
|
||||
<input type="radio" ng-model="parseType" ng-change="parseTypeChange('parseType', 'extraVars')" value="json"> <span class="parse-label">JSON</span>
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
export default ['$scope', 'Refresh', 'tagSearchService',
|
||||
function($scope, Refresh, tagSearchService) {
|
||||
export default ['$scope', 'Refresh', 'tagSearchService', '$stateParams',
|
||||
function($scope, Refresh, tagSearchService, $stateParams) {
|
||||
// JSONify passed field elements that can be searched
|
||||
$scope.list = angular.fromJson($scope.list);
|
||||
// Access config lines from list spec
|
||||
$scope.listConfig = $scope.$parent.list;
|
||||
// Grab options for the left-dropdown of the searchbar
|
||||
tagSearchService.getSearchTypes($scope.list, $scope.endpoint)
|
||||
.then(function(searchTypes) {
|
||||
$scope.searchTypes = searchTypes;
|
||||
if($stateParams.id !== "null"){
|
||||
tagSearchService.getSearchTypes($scope.list, $scope.endpoint)
|
||||
.then(function(searchTypes) {
|
||||
$scope.searchTypes = searchTypes;
|
||||
|
||||
// currently selected option of the left-dropdown
|
||||
$scope.currentSearchType = $scope.searchTypes[0];
|
||||
});
|
||||
// currently selected option of the left-dropdown
|
||||
$scope.currentSearchType = $scope.searchTypes[0];
|
||||
});
|
||||
}
|
||||
|
||||
// shows/hide the search type dropdown
|
||||
$scope.toggleTypeDropdown = function() {
|
||||
|
||||
@ -6,7 +6,10 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', fu
|
||||
var obj = {};
|
||||
// build the value (key)
|
||||
var value;
|
||||
if (field.sourceModel && field.sourceField) {
|
||||
if (field.searchField && field.filterBySearchField === true){
|
||||
value = field.searchField;
|
||||
}
|
||||
else if (field.sourceModel && field.sourceField) {
|
||||
value = field.sourceModel + '__' + field.sourceField;
|
||||
obj.related = true;
|
||||
} else if (typeof(field.key) === String) {
|
||||
@ -25,7 +28,7 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', fu
|
||||
typeOptions = field.searchOptions || [];
|
||||
} else if (field.searchType === 'boolean') {
|
||||
type = 'select';
|
||||
typeOptions = [{label: "Yes", value: true},
|
||||
typeOptions = field.typeOptions || [{label: "Yes", value: true},
|
||||
{label: "No", value: false}];
|
||||
} else {
|
||||
type = 'text';
|
||||
@ -81,41 +84,45 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', fu
|
||||
|
||||
if (needsRequest.length) {
|
||||
// make the options request to reutrn the typeOptions
|
||||
Rest.setUrl(needsRequest[0].basePath ? GetBasePath(needsRequest[0].basePath) : basePath);
|
||||
Rest.options()
|
||||
.success(function (data) {
|
||||
try {
|
||||
var options = data.actions.GET;
|
||||
needsRequest = needsRequest
|
||||
.map(function (option) {
|
||||
option.typeOptions = options[option
|
||||
.value]
|
||||
.choices
|
||||
.map(function(i) {
|
||||
return {
|
||||
value: i[0],
|
||||
label: i[1]
|
||||
};
|
||||
});
|
||||
return option;
|
||||
});
|
||||
}
|
||||
catch(err){
|
||||
if (!basePath){
|
||||
$log.error('Cannot retrieve OPTIONS because the basePath parameter is not set on the list with the following fieldset: \n', list);
|
||||
var url = needsRequest[0].basePath ? GetBasePath(needsRequest[0].basePath) : basePath;
|
||||
if(url.indexOf('null') === 0 ){
|
||||
Rest.setUrl(url);
|
||||
Rest.options()
|
||||
.success(function (data) {
|
||||
try {
|
||||
var options = data.actions.GET;
|
||||
needsRequest = needsRequest
|
||||
.map(function (option) {
|
||||
option.typeOptions = options[option
|
||||
.value]
|
||||
.choices
|
||||
.map(function(i) {
|
||||
return {
|
||||
value: i[0],
|
||||
label: i[1]
|
||||
};
|
||||
});
|
||||
return option;
|
||||
});
|
||||
}
|
||||
else { $log.error(err); }
|
||||
}
|
||||
Wait("stop");
|
||||
defer.resolve(joinOptions());
|
||||
})
|
||||
.error(function (data, status) {
|
||||
Wait("stop");
|
||||
defer.reject("options request failed");
|
||||
ProcessErrors(null, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Getting type options failed'});
|
||||
});
|
||||
catch(err){
|
||||
if (!basePath){
|
||||
$log.error('Cannot retrieve OPTIONS because the basePath parameter is not set on the list with the following fieldset: \n', list);
|
||||
}
|
||||
else { $log.error(err); }
|
||||
}
|
||||
Wait("stop");
|
||||
defer.resolve(joinOptions());
|
||||
})
|
||||
.error(function (data, status) {
|
||||
Wait("stop");
|
||||
defer.reject("options request failed");
|
||||
ProcessErrors(null, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Getting type options failed'});
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
Wait("stop");
|
||||
defer.resolve(joinOptions());
|
||||
@ -179,7 +186,7 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', fu
|
||||
this.getTag = function(field, textVal, selectVal) {
|
||||
var tag = _.clone(field);
|
||||
if (tag.type === "text") {
|
||||
tag.url = tag.value + "__icontains=" + textVal;
|
||||
tag.url = tag.value + "__icontains=" + encodeURIComponent(textVal);
|
||||
tag.name = textVal;
|
||||
} else if (selectVal.value && typeof selectVal.value === 'string' && selectVal.value.indexOf("=") > 0) {
|
||||
tag.url = selectVal.value;
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
Add passwords, SSH keys, etc. for Tower to use when launching jobs against machines, or when syncing inventories or projects.
|
||||
</p>
|
||||
</a>
|
||||
<a ui-sref="managementJobsList" class="SetupItem" ng-if="user_is_superuser">
|
||||
<a ui-sref="managementJobsList" class="SetupItem" ng-if="user_is_superuser || user_is_system_auditor">
|
||||
<h4 class="SetupItem-title">Management Jobs</h4>
|
||||
<p class="SetupItem-description">
|
||||
Manage the cleanup of old job history, activity streams, data marked for deletion, and system tracking info.
|
||||
@ -37,7 +37,7 @@
|
||||
</p>
|
||||
</a>
|
||||
<a ui-sref="notifications" class="SetupItem"
|
||||
ng-if="orgAdmin">
|
||||
ng-if="orgAdmin || user_is_system_auditor || user_is_superuser">
|
||||
<h4 class="SetupItem-title">Notifications</h4>
|
||||
<p class="SetupItem-description">
|
||||
Create templates for sending notifications with Email, HipChat, Slack, and SMS.
|
||||
|
||||
@ -1960,7 +1960,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
|
||||
// Row level actions
|
||||
if (collection.fieldActions) {
|
||||
html += "<td class=\"List-tableCell List-actionButtonCell actions\">";
|
||||
html += "<td class=\"List-actionsContainer\"><div class=\"List-tableCell List-actionButtonCell actions\">";
|
||||
for (act in collection.fieldActions) {
|
||||
fAction = collection.fieldActions[act];
|
||||
html += "<button id=\"" + ((fAction.id) ? fAction.id : act + "-action") + "\" ";
|
||||
@ -1986,7 +1986,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
//html += (fAction.label) ? "<span class=\"list-action-label\"> " + fAction.label + "</span>": "";
|
||||
html += "</button>";
|
||||
}
|
||||
html += "</td>";
|
||||
html += "</div></td>";
|
||||
html += "</tr>\n";
|
||||
}
|
||||
|
||||
|
||||
@ -137,10 +137,10 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
case 'run':
|
||||
case 'rerun':
|
||||
case 'submit':
|
||||
icon = 'fa-rocket';
|
||||
icon = 'icon-launch';
|
||||
break;
|
||||
case 'launch':
|
||||
icon = 'fa-rocket';
|
||||
icon = 'icon-launch';
|
||||
break;
|
||||
case 'stream':
|
||||
icon = 'fa-clock-o';
|
||||
|
||||
@ -524,7 +524,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
|
||||
|
||||
// Row level actions
|
||||
|
||||
innerTable += "<td class=\"List-actionButtonCell List-tableCell\">";
|
||||
innerTable += "<td class=\"List-actionsContainer\"><div class=\"List-actionButtonCell List-tableCell\">";
|
||||
|
||||
for (field_action in list.fieldActions) {
|
||||
if (field_action !== 'columnClass') {
|
||||
@ -573,7 +573,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
|
||||
}
|
||||
}
|
||||
}
|
||||
innerTable += "</td>\n";
|
||||
innerTable += "</div></td>\n";
|
||||
}
|
||||
|
||||
innerTable += "</tr>\n";
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.SmartStatus-tooltip--successful,
|
||||
.SmartStatus-tooltip--success{
|
||||
color: @default-succ;
|
||||
padding-left: 5px;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user