From 8ec2662f2e0b18ec8284294a3fa1d659b58f81d6 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 5 Dec 2016 09:45:27 -0500 Subject: [PATCH 001/595] Added templates activity stream view which is a combination of JT events and WFJT events --- awx/ui/client/src/activity-stream/activitystream.route.js | 4 ++-- .../streamDropdownNav/stream-dropdown-nav.directive.js | 8 +++++--- awx/ui/client/src/bread-crumb/bread-crumb.directive.js | 4 ++-- awx/ui/client/src/helpers/ActivityStream.js | 6 ++++++ awx/ui/client/src/helpers/ApiModel.js | 3 +++ awx/ui/client/src/templates/list/templates-list.route.js | 2 ++ awx/ui/client/src/widgets/Stream.js | 8 +++++--- 7 files changed, 25 insertions(+), 10 deletions(-) diff --git a/awx/ui/client/src/activity-stream/activitystream.route.js b/awx/ui/client/src/activity-stream/activitystream.route.js index d4af2bd0aa..d7628b5756 100644 --- a/awx/ui/client/src/activity-stream/activitystream.route.js +++ b/awx/ui/client/src/activity-stream/activitystream.route.js @@ -16,8 +16,8 @@ export default { value: { // default params will not generate search tags order_by: '-timestamp', - or__object1: null, - or__object2: null + or__object1_in: null, + or__object2_in: null } } }, diff --git a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js index 8e01af25e5..268b5b442f 100644 --- a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js +++ b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js @@ -26,7 +26,9 @@ export default ['templateUrl', function(templateUrl) { {label: 'Projects', value: 'project'}, {label: 'Schedules', value: 'schedule'}, {label: 'Teams', value: 'team'}, - {label: 'Users', value: 'user'} + {label: 'Templates', value: 'template'}, + {label: 'Users', value: 'user'}, + {label: 'Workflow Job Templates', value: 'workflow_job_template'} ]; CreateSelect2({ @@ -41,8 +43,8 @@ export default ['templateUrl', function(templateUrl) { } else { let search = _.merge($stateParams.activity_search, { - or__object1: $scope.streamTarget, - or__object2: $scope.streamTarget + or__object1__in: $scope.streamTarget && $scope.streamTarget === 'template' ? 'job_template,workflow_job_template' : $scope.streamTarget, + or__object2__in: $scope.streamTarget && $scope.streamTarget === 'template' ? 'job_template,workflow_job_template' : $scope.streamTarget }); // Attach the taget to the query parameters $state.go('activityStream', {target: $scope.streamTarget, activity_search: search}); diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js index 36fe3901ee..5cc247d232 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js +++ b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js @@ -26,8 +26,8 @@ export default if(streamConfig.activityStreamTarget) { stateGoParams.target = streamConfig.activityStreamTarget; stateGoParams.activity_search = { - or__object1: streamConfig.activityStreamTarget, - or__object2: streamConfig.activityStreamTarget, + or__object1__in: streamConfig.activityStreamTarget === 'template' ? 'job_template,workflow_job_template' : streamConfig.activityStreamTarget, + or__object2__in: streamConfig.activityStreamTarget === 'template' ? 'job_template,workflow_job_template' : streamConfig.activityStreamTarget, order_by: '-timestamp', page_size: '20', }; diff --git a/awx/ui/client/src/helpers/ActivityStream.js b/awx/ui/client/src/helpers/ActivityStream.js index 215eb00b7e..7dc8c9ecc0 100644 --- a/awx/ui/client/src/helpers/ActivityStream.js +++ b/awx/ui/client/src/helpers/ActivityStream.js @@ -52,6 +52,12 @@ export default case 'host': rtnTitle = 'HOSTS'; break; + case 'template': + rtnTitle = 'TEMPLATES'; + break; + case 'workflow_job_template': + rtnTitle = 'WORKFLOW JOB TEMPLATES'; + break; } return rtnTitle; diff --git a/awx/ui/client/src/helpers/ApiModel.js b/awx/ui/client/src/helpers/ApiModel.js index ff1923ce0a..06544675c5 100644 --- a/awx/ui/client/src/helpers/ApiModel.js +++ b/awx/ui/client/src/helpers/ApiModel.js @@ -48,6 +48,9 @@ export default case 'inventory_script': basePathKey = 'inventory_scripts'; break; + case 'workflow_job_template': + basePathKey = 'workflow_job_templates'; + break; } return basePathKey; diff --git a/awx/ui/client/src/templates/list/templates-list.route.js b/awx/ui/client/src/templates/list/templates-list.route.js index 84c6108863..e615e52db1 100644 --- a/awx/ui/client/src/templates/list/templates-list.route.js +++ b/awx/ui/client/src/templates/list/templates-list.route.js @@ -11,6 +11,8 @@ export default { label: "TEMPLATES" }, data: { + activityStream: true, + activityStreamTarget: 'template', socket: { "groups": { "jobs": ["status_changed"] diff --git a/awx/ui/client/src/widgets/Stream.js b/awx/ui/client/src/widgets/Stream.js index ccd5ad0173..5c2a6892ec 100644 --- a/awx/ui/client/src/widgets/Stream.js +++ b/awx/ui/client/src/widgets/Stream.js @@ -283,11 +283,13 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti else { // We just have a type if ($state.params.target === 'inventory_script') { - defaultUrl += '?or__object1=custom_inventory_script&or__object2=custom_inventory_script'; + defaultUrl += '?or__object1__in=custom_inventory_script&or__object2__in=custom_inventory_script'; } else if ($state.params.target === 'management_job') { - defaultUrl += '?or__object1=job&or__object2=job'; + defaultUrl += '?or__object1__in=job&or__object2__in=job'; + } else if ($state.params.target === 'template') { + defaultUrl += '?or__object1__in=job_template,workflow_job_template&or__object2__in=job_template,workflow_job_template'; } else { - defaultUrl += '?or__object1=' + $state.params.target + '&or__object2=' + $state.params.target; + defaultUrl += '?or__object1__in=' + $state.params.target + '&or__object2__in=' + $state.params.target; } } } From 101b50c888eb7ba6e291a788114904377256f2a1 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 5 Dec 2016 10:10:04 -0500 Subject: [PATCH 002/595] switch job destroy WF check to OneToOne patterns --- awx/api/views.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index dd57f1bf6e..b6bd9ae281 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1159,8 +1159,11 @@ class ProjectUpdateDetail(RetrieveDestroyAPIView): def destroy(self, request, *args, **kwargs): obj = self.get_object() - if obj.unified_job_node.filter(workflow_job__status__in=ACTIVE_STATES).exists(): - raise PermissionDenied(detail=_('Cannot delete job resource when associated workflow job is running.')) + try: + if obj.unified_job_node.workflow_job.status in ACTIVE_STATES: + raise PermissionDenied(detail=_('Cannot delete job resource when associated workflow job is running.')) + except ProjectUpdate.unified_job_node.RelatedObjectDoesNotExist: + pass return super(ProjectUpdateDetail, self).destroy(request, *args, **kwargs) @@ -2239,8 +2242,11 @@ class InventoryUpdateDetail(RetrieveDestroyAPIView): def destroy(self, request, *args, **kwargs): obj = self.get_object() - if obj.unified_job_node.filter(workflow_job__status__in=ACTIVE_STATES).exists(): - raise PermissionDenied(detail=_('Cannot delete job resource when associated workflow job is running.')) + try: + if obj.unified_job_node.workflow_job.status in ACTIVE_STATES: + raise PermissionDenied(detail=_('Cannot delete job resource when associated workflow job is running.')) + except InventoryUpdate.unified_job_node.RelatedObjectDoesNotExist: + pass return super(InventoryUpdateDetail, self).destroy(request, *args, **kwargs) @@ -3205,8 +3211,11 @@ class JobDetail(RetrieveUpdateDestroyAPIView): def destroy(self, request, *args, **kwargs): obj = self.get_object() - if obj.unified_job_node.filter(workflow_job__status__in=ACTIVE_STATES).exists(): - raise PermissionDenied(detail=_('Cannot delete job resource when associated workflow job is running.')) + try: + if obj.unified_job_node.workflow_job.status in ACTIVE_STATES: + raise PermissionDenied(detail=_('Cannot delete job resource when associated workflow job is running.')) + except Job.unified_job_node.RelatedObjectDoesNotExist: + pass return super(JobDetail, self).destroy(request, *args, **kwargs) From 14746eebe34444c4ed90c29c4ddbfa2692564b03 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 5 Dec 2016 11:21:29 -0500 Subject: [PATCH 003/595] fix system auditor getter logic --- awx/main/models/__init__.py | 3 ++- awx/main/tests/functional/conftest.py | 9 ++++++++- awx/main/tests/functional/test_rbac_user.py | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index b962456e48..d8b206e18a 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -76,7 +76,8 @@ User.add_to_class('auditor_of_organizations', user_get_auditor_of_organizations) @property def user_is_system_auditor(user): if not hasattr(user, '_is_system_auditor'): - user._is_system_auditor = Role.objects.filter(role_field='system_auditor', id=user.id).exists() + user._is_system_auditor = user.roles.filter( + singleton_name='system_auditor', role_field='system_auditor').exists() return user._is_system_auditor diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 05f1941fab..165dfed0d6 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -41,7 +41,7 @@ from awx.main.models.organization import ( Permission, Team, ) - +from awx.main.models.rbac import Role from awx.main.models.notifications import ( NotificationTemplate, Notification @@ -262,6 +262,13 @@ def admin(user): return user('admin', True) +@pytest.fixture +def system_auditor(user): + u = user(False) + Role.singleton('system_auditor').members.add(u) + return u + + @pytest.fixture def alice(user): return user('alice', False) diff --git a/awx/main/tests/functional/test_rbac_user.py b/awx/main/tests/functional/test_rbac_user.py index 0b43ce2f1c..c7eaa8c0e9 100644 --- a/awx/main/tests/functional/test_rbac_user.py +++ b/awx/main/tests/functional/test_rbac_user.py @@ -9,7 +9,7 @@ from awx.main.models import Role, User, Organization, Inventory @pytest.mark.django_db -class TestSysAuditor(TransactionTestCase): +class TestSysAuditorTransactional(TransactionTestCase): def rando(self): return User.objects.create(username='rando', password='rando', email='rando@com.com') @@ -41,6 +41,10 @@ class TestSysAuditor(TransactionTestCase): assert not rando.is_system_auditor +@pytest.mark.django_db +def test_system_auditor_is_system_auditor(system_auditor): + assert system_auditor.is_system_auditor + @pytest.mark.django_db def test_user_admin(user_project, project, user): From 057efd3d5abaae955aa9542565a832bda1955b8d Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 5 Dec 2016 11:40:26 -0500 Subject: [PATCH 004/595] avoid applying system auditor prop if action was association --- awx/api/views.py | 2 +- .../functional/api/test_create_attach_views.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/awx/api/views.py b/awx/api/views.py index dd57f1bf6e..6a1c24bad4 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -768,7 +768,7 @@ class BaseUsersList(SubListCreateAttachDetachAPIView): def post(self, request, *args, **kwargs): ret = super(BaseUsersList, self).post( request, *args, **kwargs) try: - if request.data.get('is_system_auditor', False): + if ret.data is not None and request.data.get('is_system_auditor', False): # This is a faux-field that just maps to checking the system # auditor role member list.. unfortunately this means we can't # set it on creation, and thus needs to be set here. diff --git a/awx/main/tests/functional/api/test_create_attach_views.py b/awx/main/tests/functional/api/test_create_attach_views.py index 48f3aadc7b..b80cb4fa2c 100644 --- a/awx/main/tests/functional/api/test_create_attach_views.py +++ b/awx/main/tests/functional/api/test_create_attach_views.py @@ -45,3 +45,18 @@ def test_role_team_view_access(rando, team, inventory, mocker, post): mock_access.assert_called_once_with( inventory.admin_role, team, 'member_role.parents', data, skip_sub_obj_read_check=False) + + +@pytest.mark.django_db +def test_org_associate_with_junk_data(rando, admin_user, organization, post): + """ + Assure that post-hoc enforcement of auditor role + will turn off if the action is an association + """ + user_data = {'is_system_auditor': True, 'id': rando.pk} + post(url=reverse('api:organization_users_list', args=(organization.pk,)), + data=user_data, expect=204, user=admin_user) + # assure user is now an org member + assert rando in organization.member_role + # assure that this did not also make them a system auditor + assert not rando.is_system_auditor From 8c64fa2fe1f318116ac213f22590f863edf67c71 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 5 Dec 2016 12:05:00 -0500 Subject: [PATCH 005/595] handle is_system_auditor case with unsaved users --- awx/main/models/__init__.py | 8 +++- awx/main/tests/functional/api/test_user.py | 56 +++++++--------------- 2 files changed, 22 insertions(+), 42 deletions(-) diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index d8b206e18a..0475da8eb7 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -76,8 +76,12 @@ User.add_to_class('auditor_of_organizations', user_get_auditor_of_organizations) @property def user_is_system_auditor(user): if not hasattr(user, '_is_system_auditor'): - user._is_system_auditor = user.roles.filter( - singleton_name='system_auditor', role_field='system_auditor').exists() + if user.pk: + user._is_system_auditor = user.roles.filter( + singleton_name='system_auditor', role_field='system_auditor').exists() + else: + # Odd case where user is unsaved, this should never be relied on + user._is_system_auditor = False return user._is_system_auditor diff --git a/awx/main/tests/functional/api/test_user.py b/awx/main/tests/functional/api/test_user.py index fb4c7a33e0..e3b7b4145c 100644 --- a/awx/main/tests/functional/api/test_user.py +++ b/awx/main/tests/functional/api/test_user.py @@ -7,65 +7,41 @@ from django.core.urlresolvers import reverse # user creation # +EXAMPLE_USER_DATA = { + "username": "affable", + "first_name": "a", + "last_name": "a", + "email": "a@a.com", + "is_superuser": False, + "password": "r$TyKiOCb#ED" +} + @pytest.mark.django_db def test_user_create(post, admin): - response = post(reverse('api:user_list'), { - "username": "affable", - "first_name": "a", - "last_name": "a", - "email": "a@a.com", - "is_superuser": False, - "password": "fo0m4nchU" - }, admin) + response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin) assert response.status_code == 201 + assert not response.data['is_superuser'] + assert not response.data['is_system_auditor'] @pytest.mark.django_db def test_fail_double_create_user(post, admin): - response = post(reverse('api:user_list'), { - "username": "affable", - "first_name": "a", - "last_name": "a", - "email": "a@a.com", - "is_superuser": False, - "password": "fo0m4nchU" - }, admin) + response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin) assert response.status_code == 201 - response = post(reverse('api:user_list'), { - "username": "affable", - "first_name": "a", - "last_name": "a", - "email": "a@a.com", - "is_superuser": False, - "password": "fo0m4nchU" - }, admin) + response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin) assert response.status_code == 400 @pytest.mark.django_db def test_create_delete_create_user(post, delete, admin): - response = post(reverse('api:user_list'), { - "username": "affable", - "first_name": "a", - "last_name": "a", - "email": "a@a.com", - "is_superuser": False, - "password": "fo0m4nchU" - }, admin) + response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin) assert response.status_code == 201 response = delete(reverse('api:user_detail', args=(response.data['id'],)), admin) assert response.status_code == 204 - response = post(reverse('api:user_list'), { - "username": "affable", - "first_name": "a", - "last_name": "a", - "email": "a@a.com", - "is_superuser": False, - "password": "fo0m4nchU" - }, admin) + response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin) print(response.data) assert response.status_code == 201 From 0255b9483d7ed047141d3e4b0f7f57bb346c85f7 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 5 Dec 2016 13:27:17 -0500 Subject: [PATCH 006/595] add workflow jobs to UJ options --- awx/api/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 9210f6abe9..8b95bfc954 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -250,6 +250,7 @@ class BaseSerializer(serializers.ModelSerializer): 'project_update': _('SCM Update'), 'inventory_update': _('Inventory Sync'), 'system_job': _('Management Job'), + 'workflow_job': _('Workflow Job'), } choices = [] for t in self.get_types(): @@ -666,7 +667,7 @@ class UnifiedJobListSerializer(UnifiedJobSerializer): def get_types(self): if type(self) is UnifiedJobListSerializer: - return ['project_update', 'inventory_update', 'job', 'ad_hoc_command', 'system_job'] + return ['project_update', 'inventory_update', 'job', 'ad_hoc_command', 'system_job', 'workflow_job'] else: return super(UnifiedJobListSerializer, self).get_types() From f47f8abe42dda4b6b3962c443d12568474d3ae28 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 5 Dec 2016 13:29:59 -0500 Subject: [PATCH 007/595] finished fixing job through workflow node related to #4182 --- awx/api/serializers.py | 6 +++++- awx/main/models/unified_jobs.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 9210f6abe9..01ab3c6fac 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -607,7 +607,11 @@ class UnifiedJobSerializer(BaseSerializer): summary_fields = super(UnifiedJobSerializer, self).get_summary_fields(obj) if obj.spawned_by_workflow: summary_fields['source_workflow_job'] = {} - summary_obj = obj.unified_job_node.workflow_job + try: + summary_obj = obj.unified_job_node.workflow_job + except ObjectDoesNotExist: + return summary_fields + for field in SUMMARIZABLE_FK_FIELDS['job']: val = getattr(summary_obj, field, None) if val is not None: diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 63c3e3196a..712bb73530 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -20,6 +20,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now from django.utils.encoding import smart_text from django.apps import apps +from django.core.exceptions import ObjectDoesNotExist # Django-Polymorphic from polymorphic import PolymorphicModel @@ -780,13 +781,19 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique @property def workflow_job_id(self): if self.spawned_by_workflow: - return self.unified_job_node.workflow_job.pk + try: + return self.unified_job_node.workflow_job.pk + except ObjectDoesNotExist: + pass return None @property def workflow_node_id(self): if self.spawned_by_workflow: - return self.unified_job_node.pk + try: + return self.unified_job_node.pk + except ObjectDoesNotExist: + pass return None @property From a5b1c7b57994c1c98be2f07a6489cdf135166d5d Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 5 Dec 2016 13:54:33 -0500 Subject: [PATCH 008/595] use more precise exception --- awx/api/serializers.py | 2 +- awx/main/models/unified_jobs.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 01ab3c6fac..2f98fc6053 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -609,7 +609,7 @@ class UnifiedJobSerializer(BaseSerializer): summary_fields['source_workflow_job'] = {} try: summary_obj = obj.unified_job_node.workflow_job - except ObjectDoesNotExist: + except UnifiedJob.unified_job_node.RelatedObjectDoesNotExist: return summary_fields for field in SUMMARIZABLE_FK_FIELDS['job']: diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 712bb73530..31afe69d32 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -20,7 +20,6 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now from django.utils.encoding import smart_text from django.apps import apps -from django.core.exceptions import ObjectDoesNotExist # Django-Polymorphic from polymorphic import PolymorphicModel @@ -783,7 +782,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique if self.spawned_by_workflow: try: return self.unified_job_node.workflow_job.pk - except ObjectDoesNotExist: + except UnifiedJob.unified_job_node.RelatedObjectDoesNotExist: pass return None @@ -792,7 +791,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique if self.spawned_by_workflow: try: return self.unified_job_node.pk - except ObjectDoesNotExist: + except UnifiedJob.unified_job_node.RelatedObjectDoesNotExist: pass return None From 0706ae59819d965e529fe09ec1aea1b37fc54a68 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 5 Dec 2016 15:02:18 -0500 Subject: [PATCH 009/595] Recognize edge conflicts when they arise and force the user to fix them before saving. --- awx/ui/client/src/forms/WorkflowMaker.js | 10 +-- .../workflow-chart/workflow-chart.block.less | 7 ++ .../workflow-chart.directive.js | 19 ++++- .../workflow-maker.controller.js | 78 ++++++++++++++++--- .../workflow-maker.partial.html | 2 +- .../templates/workflows/workflow.service.js | 42 +++++++++- 6 files changed, 138 insertions(+), 20 deletions(-) diff --git a/awx/ui/client/src/forms/WorkflowMaker.js b/awx/ui/client/src/forms/WorkflowMaker.js index 9b1c369e32..5bd7788db1 100644 --- a/awx/ui/client/src/forms/WorkflowMaker.js +++ b/awx/ui/client/src/forms/WorkflowMaker.js @@ -33,27 +33,27 @@ export default edgeType: { label: i18n._('Type'), type: 'radio_group', - ngShow: 'selectedTemplate && showTypeOptions', + ngShow: 'selectedTemplate && edgeFlags.showTypeOptions', ngDisabled: '!canAddWorkflowJobTemplate', options: [ { label: i18n._('On Success'), value: 'success', - ngShow: '!edgeTypeRestriction || edgeTypeRestriction === "successFailure"' + ngShow: '!edgeFlags.typeRestriction || edgeFlags.typeRestriction === "successFailure"' }, { label: i18n._('On Failure'), value: 'failure', - ngShow: '!edgeTypeRestriction || edgeTypeRestriction === "successFailure"' + ngShow: '!edgeFlags.typeRestriction || edgeFlags.typeRestriction === "successFailure"' }, { label: i18n._('Always'), value: 'always', - ngShow: '!edgeTypeRestriction || edgeTypeRestriction === "always"' + ngShow: '!edgeFlags.typeRestriction || edgeFlags.typeRestriction === "always"' } ], awRequiredWhen: { - reqExpression: 'showTypeOptions' + reqExpression: 'edgeFlags.showTypeOptions' } }, credential: { diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less index 4c795d2a40..177ab6b35d 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less @@ -90,3 +90,10 @@ width: 90px; color: @default-interface-txt; } +.WorkflowChart-conflictIcon { + color: @default-err; +} +.WorkflowChart-conflictText { + width: 90px; + color: @default-interface-txt; +} diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index ac8e9c44ac..9e5ad95475 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -119,8 +119,7 @@ export default [ '$state', .attr("class", "node") .attr("id", function(d){return "node-" + d.id;}) .attr("parent", function(d){return d.parent ? d.parent.id : null;}) - .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }) - .attr("fill", "red"); + .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); nodeEnter.each(function(d) { let thisNode = d3.select(this); @@ -171,6 +170,16 @@ export default [ '$state', return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : ""; }).each(wrap); + thisNode.append("foreignObject") + .attr("x", 43) + .attr("y", 45) + .style("font-size","0.7em") + .attr("class", "WorkflowChart-conflictText") + .html(function () { + return "\uf06a EDGE CONFLICT"; + }) + .style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; }); + thisNode.append("foreignObject") .attr("x", 17) .attr("y", 22) @@ -347,7 +356,7 @@ export default [ '$state', let link = svgGroup.selectAll("g.link") .data(links, function(d) { - return d.target.id; + return d.source.id + "-" + d.target.id; }); let linkEnter = link.enter().append("g") @@ -485,6 +494,7 @@ export default [ '$state', }); t.selectAll(".node") + .attr("parent", function(d){return d.parent ? d.parent.id : null;}) .attr("transform", function(d) {d.px = d.x; d.py = d.y; return "translate(" + d.y + "," + d.x + ")"; }); t.selectAll(".WorkflowChart-nodeTypeCircle") @@ -558,6 +568,9 @@ export default [ '$state', t.selectAll(".WorkflowChart-incompleteText") .style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; }); + t.selectAll(".WorkflowChart-conflictText") + .style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; }); + } function add_node() { diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js index 7414826f53..e6bd83d9b3 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js @@ -29,6 +29,12 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr value: "check" }]; + $scope.edgeFlags = { + conflict: false, + typeRestriction: null, + showTypeOptions: false + }; + function init() { $scope.treeDataMaster = angular.copy($scope.treeData.data); $scope.$broadcast("refreshWorkflowChart"); @@ -36,7 +42,7 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr function resetNodeForm() { $scope.workflowMakerFormConfig.nodeMode = "idle"; - $scope.showTypeOptions = false; + $scope.edgeFlags.showTypeOptions = false; delete $scope.selectedTemplate; delete $scope.workflow_job_templates; delete $scope.workflow_projects; @@ -44,7 +50,7 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr delete $scope.placeholderNode; delete $scope.betweenTwoNodes; $scope.nodeBeingEdited = null; - $scope.edgeTypeRestriction = null; + $scope.edgeFlags.typeRestriction = null; $scope.workflowMakerFormConfig.activeTab = "jobs"; } @@ -89,7 +95,8 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({ tree: $scope.treeData.data, - parentId: betweenTwoNodes ? parent.source.id : parent.id + parentId: betweenTwoNodes ? parent.source.id : parent.id, + childId: $scope.placeholderNode.id }); // Set the default to success @@ -99,21 +106,27 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr // We don't want to give the user the option to select // a type as this node will always be executed edgeType = "always"; - $scope.showTypeOptions = false; + $scope.edgeFlags.showTypeOptions = false; } else { if ((_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) && _.includes(siblingConnectionTypes, "always")) { - // This is a problem... + // This is a conflicted scenario but we'll just let the user keep building - they will have to remediate before saving + $scope.edgeFlags.typeRestriction = null; } else if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) { - $scope.edgeTypeRestriction = "successFailure"; + $scope.edgeFlags.typeRestriction = "successFailure"; edgeType = "success"; } else if (_.includes(siblingConnectionTypes, "always")) { - $scope.edgeTypeRestriction = "always"; + $scope.edgeFlags.typeRestriction = "always"; edgeType = "always"; + } else { + $scope.edgeFlags.typeRestriction = null; } - $scope.showTypeOptions = true; + $scope.edgeFlags.showTypeOptions = true; } + // Reset the edgeConflict flag + resetEdgeConflict(); + $scope.$broadcast("setEdgeType", edgeType); $scope.$broadcast("refreshWorkflowChart"); @@ -181,6 +194,9 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr } } + // Reset the edgeConflict flag + resetEdgeConflict(); + $scope.$broadcast("refreshWorkflowChart"); }; @@ -195,6 +211,9 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr $scope.nodeBeingEdited.isActiveEdit = false; } + // Reset the edgeConflict flag + resetEdgeConflict(); + // Reset the form resetNodeForm(); @@ -208,6 +227,12 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr if (!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) { if ($scope.placeholderNode || $scope.nodeBeingEdited) { $scope.cancelNodeForm(); + + // Refresh this object as the parent has changed + nodeToEdit = WorkflowService.searchTree({ + element: $scope.treeData.data, + matchingId: nodeToEdit.id + }); } $scope.workflowMakerFormConfig.nodeMode = "edit"; @@ -330,7 +355,30 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr } } - $scope.showTypeOptions = (parent && parent.isStartNode) ? false : true; + let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({ + tree: $scope.treeData.data, + parentId: parent.id, + childId: nodeToEdit.id + }); + + if (parent && parent.isStartNode) { + // We don't want to give the user the option to select + // a type as this node will always be executed + $scope.edgeFlags.showTypeOptions = false; + } else { + if ((_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) && _.includes(siblingConnectionTypes, "always")) { + // This is a conflicted scenario but we'll just let the user keep building - they will have to remediate before saving + $scope.edgeFlags.typeRestriction = null; + } else if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure") && (nodeToEdit.edgeType === "success" || nodeToEdit.edgeType === "failure")) { + $scope.edgeFlags.typeRestriction = "successFailure"; + } else if (_.includes(siblingConnectionTypes, "always") && nodeToEdit.edgeType === "always") { + $scope.edgeFlags.typeRestriction = "always"; + } else { + $scope.edgeFlags.typeRestriction = null; + } + + $scope.edgeFlags.showTypeOptions = true; + } $scope.$broadcast('setEdgeType', $scope.nodeBeingEdited.edgeType); @@ -441,6 +489,9 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr resetNodeForm(); } + // Reset the edgeConflict flag + resetEdgeConflict(); + resetDeleteNode(); $scope.$broadcast("refreshWorkflowChart"); @@ -515,6 +566,15 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr }); }; + function resetEdgeConflict(){ + $scope.edgeFlags.conflict = false; + + WorkflowService.checkForEdgeConflicts({ + treeData: $scope.treeData.data, + edgeFlags: $scope.edgeFlags + }); + } + init(); } diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html index b52b8ed381..f3ea67b990 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html @@ -82,6 +82,6 @@
- +
diff --git a/awx/ui/client/src/templates/workflows/workflow.service.js b/awx/ui/client/src/templates/workflows/workflow.service.js index 36282fac29..4e551c7582 100644 --- a/awx/ui/client/src/templates/workflows/workflow.service.js +++ b/awx/ui/client/src/templates/workflows/workflow.service.js @@ -46,6 +46,8 @@ export default [function(){ child.edgeType = "always"; } + child.parent = parentNode; + parentNode.children.push(child); }); } @@ -81,9 +83,10 @@ export default [function(){ if(params.betweenTwoNodes) { _.forEach(parentNode.children, function(child, index) { if(child.id === params.parent.target.id) { - placeholder.children.push(angular.copy(child)); + placeholder.children.push(child); parentNode.children[index] = placeholder; placeholderRef = parentNode.children[index]; + child.parent = parentNode.children[index]; return false; } }); @@ -102,6 +105,7 @@ export default [function(){ }, getSiblingConnectionTypes: function(params) { // params.parentId + // params.childId // params.tree let siblingConnectionTypes = {}; @@ -114,7 +118,7 @@ export default [function(){ if(parentNode.children && parentNode.children.length > 0) { // Loop across them and add the types as keys to siblingConnectionTypes _.forEach(parentNode.children, function(child) { - if(!child.placeholder && child.edgeType) { + if(child.id !== params.childId && !child.placeholder && child.edgeType) { siblingConnectionTypes[child.edgeType] = true; } }); @@ -283,6 +287,40 @@ export default [function(){ }; } + }, + checkForEdgeConflicts: function(params) { + //params.treeData + //params.edgeFlags + + let hasAlways = false; + let hasSuccessFailure = false; + let _this = this; + + _.forEach(params.treeData.children, function(child) { + // Flip the flag to false for now - we'll set it to true later on + // if we detect a conflict + child.edgeConflict = false; + if(child.edgeType === 'always') { + hasAlways = true; + } + else if(child.edgeType === 'success' || child.edgeType === 'failure') { + hasSuccessFailure = true; + } + + _this.checkForEdgeConflicts({ + treeData: child, + edgeFlags: params.edgeFlags + }); + }); + + if(hasAlways && hasSuccessFailure) { + // We have a conflict + _.forEach(params.treeData.children, function(child) { + child.edgeConflict = true; + }); + + params.edgeFlags.conflict = true; + } } }; }]; From 4b5e131362a4392617cd3d1b90ff642a6cecedec Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 5 Dec 2016 15:17:34 -0500 Subject: [PATCH 010/595] allow org admins to share credentials between orgs --- awx/main/access.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 7f28e0a7ce..55b0185e53 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -837,15 +837,7 @@ class CredentialAccess(BaseAccess): def can_change(self, obj, data): if not obj: return False - - # Cannot change the organization for a credential after it's been created - if data and 'organization' in data: - organization_pk = get_pk_from_dict(data, 'organization') - if (organization_pk and (not obj.organization or organization_pk != obj.organization.id)) \ - or (not organization_pk and obj.organization): - return False - - return self.user in obj.admin_role + return self.user in obj.admin_role and self.check_related('organization', Organization, data, obj=obj) def can_delete(self, obj): # Unassociated credentials may be marked deleted by anyone, though we From e81dca3249b910b1c9a7c7ea0228a5a76e1efc2b Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 5 Dec 2016 16:00:54 -0500 Subject: [PATCH 011/595] allow reading of credential org field in list view --- awx/api/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 0f6e7410f6..f15e762c03 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1762,9 +1762,9 @@ class CredentialSerializerCreate(CredentialSerializer): 'do not give either user or organization. Only valid for creation.')) organization = serializers.PrimaryKeyRelatedField( queryset=Organization.objects.all(), - required=False, default=None, write_only=True, allow_null=True, - help_text=_('Write-only field used to add organization to owner role. If provided, ' - 'do not give either team or team. Only valid for creation.')) + required=False, default=None, allow_null=True, + help_text=_('Inherit permissions from organization roles. If provided on creation, ' + 'do not give either user or team.')) class Meta: model = Credential From 69e7ef711011488770bf05c643c13f1a8ea187f2 Mon Sep 17 00:00:00 2001 From: Ryan Fitzpatrick Date: Mon, 5 Dec 2016 16:14:46 -0500 Subject: [PATCH 012/595] Include pyrax in ansible requirements (#4283) * Include pyrax in ansible requirements * generate requirements_ansible.txt * Add secretstorage backend in requirements_ansible.in --- requirements/requirements_ansible.in | 1 + requirements/requirements_ansible.txt | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/requirements/requirements_ansible.in b/requirements/requirements_ansible.in index 2e2085985b..98981f2571 100644 --- a/requirements/requirements_ansible.in +++ b/requirements/requirements_ansible.in @@ -4,4 +4,5 @@ azure==2.0.0rc6 kombu==3.0.35 boto==2.43.0 psutil==5.0.0 +secretstorage==2.3.1 shade==1.13.1 diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index d11dac50a1..acba0d65ae 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -4,12 +4,13 @@ # # pip-compile --output-file requirements_ansible.txt requirements_ansible.in # +-e git+https://github.com/chrismeyersfsu/pyrax@tower#egg=pyrax amqp==1.4.9 # via kombu anyjson==0.3.3 # via kombu apache-libcloud==1.3.0 appdirs==1.4.0 # via os-client-config, python-ironicclient azure-batch==1.0.0 # via azure -azure-common[autorest]==1.1.4 # via azure-batch, azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-keyvault, azure-mgmt-logic, azure-mgmt-network, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-storage, azure-servicebus, azure-servicemanagement-legacy, azure-storage +azure-common==1.1.4 # via azure-batch, azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-keyvault, azure-mgmt-logic, azure-mgmt-network, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-storage, azure-servicebus, azure-servicemanagement-legacy, azure-storage azure-mgmt-batch==1.0.0 # via azure-mgmt azure-mgmt-compute==0.30.0rc6 # via azure-mgmt azure-mgmt-keyvault==0.30.0rc6 # via azure-mgmt @@ -26,7 +27,7 @@ azure-servicebus==0.20.3 # via azure azure-servicemanagement-legacy==0.20.4 # via azure azure-storage==0.33.0 # via azure azure==2.0.0rc6 -Babel==2.3.4 # via osc-lib, oslo.i18n, python-cinderclient, python-glanceclient, python-heatclient, python-magnumclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient +babel==2.3.4 # via osc-lib, oslo.i18n, python-cinderclient, python-glanceclient, python-heatclient, python-magnumclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient boto==2.43.0 certifi==2016.9.26 # via msrest cffi==1.9.1 # via cryptography @@ -74,7 +75,7 @@ oslo.serialization==2.14.0 # via python-heatclient, python-ironicclient, python oslo.utils==3.18.0 # via osc-lib, oslo.serialization, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient pbr==1.10.0 # via cliff, debtcollector, keystoneauth1, mock, openstacksdk, osc-lib, oslo.i18n, oslo.serialization, oslo.utils, positional, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient, requestsexceptions, shade, stevedore positional==1.1.1 # via keystoneauth1, python-keystoneclient -PrettyTable==0.7.2 # via cliff, python-cinderclient, python-glanceclient, python-heatclient, python-ironicclient, python-magnumclient, python-novaclient, python-troveclient +prettytable==0.7.2 # via cliff, python-cinderclient, python-glanceclient, python-heatclient, python-ironicclient, python-magnumclient, python-novaclient, python-troveclient psutil==5.0.0 pyasn1==0.1.9 # via cryptography pycparser==2.17 # via cffi @@ -94,7 +95,7 @@ python-openstackclient==3.4.1 # via python-ironicclient python-swiftclient==3.2.0 # via python-heatclient, python-troveclient, shade python-troveclient==2.6.0 # via shade pytz==2016.7 # via babel, oslo.serialization, oslo.utils -PyYAML==3.12 # via cliff, os-client-config, python-heatclient, python-ironicclient, python-mistralclient +pyyaml==3.12 # via cliff, os-client-config, python-heatclient, python-ironicclient, python-mistralclient rackspace-auth-openstack==1.3 # via rackspace-novaclient rackspace-novaclient==2.1 rax-default-network-flags-python-novaclient-ext==0.4.0 # via rackspace-novaclient @@ -103,7 +104,7 @@ requests-oauthlib==0.7.0 # via msrest requests==2.12.1 # via azure-servicebus, azure-servicemanagement-legacy, azure-storage, keystoneauth1, msrest, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-swiftclient, python-troveclient, requests-oauthlib requestsexceptions==1.1.3 # via os-client-config, shade rfc3986==0.4.1 # via oslo.config -secretstorage==2.3.1 # via keyring +secretstorage==2.3.1 shade==1.13.1 simplejson==3.10.0 # via osc-lib, python-cinderclient, python-neutronclient, python-novaclient, python-troveclient six==1.10.0 # via cliff, cryptography, debtcollector, keystoneauth1, mock, openstacksdk, osc-lib, oslo.config, oslo.i18n, oslo.serialization, oslo.utils, python-cinderclient, python-dateutil, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-openstackclient, python-swiftclient, python-troveclient, shade, stevedore, warlock From 81cb57be4f50c647b93be27440d25690937c0513 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 5 Dec 2016 16:17:58 -0500 Subject: [PATCH 013/595] remove tests pertaining to credential org related field --- .../tests/functional/api/test_credential.py | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/awx/main/tests/functional/api/test_credential.py b/awx/main/tests/functional/api/test_credential.py index bd6cd25841..8f596cdac9 100644 --- a/awx/main/tests/functional/api/test_credential.py +++ b/awx/main/tests/functional/api/test_credential.py @@ -339,39 +339,6 @@ def test_list_created_org_credentials(post, get, organization, org_admin, org_me assert response.data['count'] == 0 -@pytest.mark.django_db -def test_cant_change_organization(patch, credential, organization, org_admin): - credential.organization = organization - credential.save() - - response = patch(reverse('api:credential_detail', args=(credential.id,)), { - 'name': 'Some new name', - }, org_admin) - assert response.status_code == 200 - - response = patch(reverse('api:credential_detail', args=(credential.id,)), { - 'name': 'Some new name2', - 'organization': organization.id, # fine for it to be the same - }, org_admin) - assert response.status_code == 200 - - response = patch(reverse('api:credential_detail', args=(credential.id,)), { - 'name': 'Some new name3', - 'organization': None - }, org_admin) - assert response.status_code == 403 - - -@pytest.mark.django_db -def test_cant_add_organization(patch, credential, organization, org_admin): - assert credential.organization is None - response = patch(reverse('api:credential_detail', args=(credential.id,)), { - 'name': 'Some new name', - 'organization': organization.id - }, org_admin) - assert response.status_code == 403 - - # # Openstack Credentials # From 1daeff8db1c3305feb6f914f2c1d025feba9ee64 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 5 Dec 2016 17:01:50 -0500 Subject: [PATCH 014/595] unsaved user system auditor status made more conservative --- awx/main/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 0475da8eb7..3f7e309940 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -81,7 +81,7 @@ def user_is_system_auditor(user): singleton_name='system_auditor', role_field='system_auditor').exists() else: # Odd case where user is unsaved, this should never be relied on - user._is_system_auditor = False + return False return user._is_system_auditor From fe29f5e0c992c1e060ea97cd77757d564f2425d0 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 5 Dec 2016 19:46:36 -0800 Subject: [PATCH 015/595] Redirect to license page if no license This was broken due to the upgrade to UI-Router v.1.0.0-beta.3. I also tracked down the $stateChangeStart function in app.js that when missing some time in 3.1 development --- awx/ui/client/src/app.js | 54 +++++++++++++++++-- .../src/license/checkLicense.factory.js | 4 +- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index d903776765..ec6639ce7b 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -385,6 +385,11 @@ var tower = angular.module('Tower', [ }; $rootScope.$stateParams = $stateParams; + $state.defaultErrorHandler(function() { + // Do not log transitionTo errors + // $log.debug("transitionTo error: " + error ); + }); + I18NInit(); $stateExtender.addState({ name: 'dashboard', @@ -681,9 +686,52 @@ var tower = angular.module('Tower', [ $rootScope.crumbCache = []; - // $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState) { - // SocketService.subscribe(toState, toParams); - // }); + $rootScope.$on("$stateChangeStart", function (event, next) { + // Remove any lingering intervals + // except on jobDetails.* states + var jobDetailStates = [ + 'jobDetail', + 'jobDetail.host-summary', + 'jobDetail.host-event.details', + 'jobDetail.host-event.json', + 'jobDetail.host-events', + 'jobDetail.host-event.stdout' + ]; + if ($rootScope.jobDetailInterval && !_.includes(jobDetailStates, next.name) ) { + window.clearInterval($rootScope.jobDetailInterval); + } + if ($rootScope.jobStdOutInterval && !_.includes(jobDetailStates, next.name) ) { + window.clearInterval($rootScope.jobStdOutInterval); + } + + // On each navigation request, check that the user is logged in + if (!/^\/(login|logout)/.test($location.path())) { + // capture most recent URL, excluding login/logout + $rootScope.lastPath = $location.path(); + $rootScope.enteredPath = $location.path(); + $cookieStore.put('lastPath', $location.path()); + } + + if (Authorization.isUserLoggedIn() === false) { + if (next.name !== "signIn") { + $state.go('signIn'); + } + } else if ($rootScope && $rootScope.sessionTimer && $rootScope.sessionTimer.isExpired()) { + // gets here on timeout + if (next.name !== "signIn") { + $state.go('signIn'); + } + } else { + if ($rootScope.current_user === undefined || $rootScope.current_user === null) { + Authorization.restoreUserInfo(); //user must have hit browser refresh + } + if (next && (next.name !== "signIn" && next.name !== "signOut" && next.name !== "license")) { + // if not headed to /login or /logout, then check the license + CheckLicense.test(event); + } + } + activateTab(); + }); $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState) { diff --git a/awx/ui/client/src/license/checkLicense.factory.js b/awx/ui/client/src/license/checkLicense.factory.js index d68358cd29..c104ce091c 100644 --- a/awx/ui/client/src/license/checkLicense.factory.js +++ b/awx/ui/client/src/license/checkLicense.factory.js @@ -42,10 +42,10 @@ export default if(license === null || !$rootScope.license_tested){ if(this.valid(license) === false) { $rootScope.licenseMissing = true; + $state.go('license'); if(event){ event.preventDefault(); } - $state.go('license'); } else { $rootScope.licenseMissing = false; @@ -53,7 +53,7 @@ export default } else if(this.valid(license) === false) { $rootScope.licenseMissing = true; - $state.transitionTo('license'); + $state.go('license'); if(event){ event.preventDefault(); } From 5d5fe88a47b094221f2f7ee6c2d3c4bf90f053f9 Mon Sep 17 00:00:00 2001 From: Graham Mainwaring Date: Mon, 5 Dec 2016 16:25:11 -0500 Subject: [PATCH 016/595] Move npm dependencies to Ansible github org --- awx/ui/npm-shrinkwrap.json | 51 +++++++++++++++++++++++++------------- awx/ui/package.json | 20 ++++++++------- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/awx/ui/npm-shrinkwrap.json b/awx/ui/npm-shrinkwrap.json index 240a18f337..04f81e0e75 100644 --- a/awx/ui/npm-shrinkwrap.json +++ b/awx/ui/npm-shrinkwrap.json @@ -61,13 +61,13 @@ }, "angular-breadcrumb": { "version": "0.4.1", - "from": "leigh-johnson/angular-breadcrumb#0.4.1", - "resolved": "git://github.com/leigh-johnson/angular-breadcrumb.git#6c2b1ad45ad5fbe7adf39af1ef3b294ca8e207a9" + "from": "ansible/angular-breadcrumb#0.4.1", + "resolved": "git://github.com/ansible/angular-breadcrumb.git#6c2b1ad45ad5fbe7adf39af1ef3b294ca8e207a9" }, "angular-codemirror": { "version": "1.0.4", - "from": "chouseknecht/angular-codemirror#1.0.4", - "resolved": "git://github.com/chouseknecht/angular-codemirror.git#75c3a2d0ccdf2e4c836fab7d7617d5db6c585c1b", + "from": "ansible/angular-codemirror#1.0.4", + "resolved": "git://github.com/ansible/angular-codemirror.git#75c3a2d0ccdf2e4c836fab7d7617d5db6c585c1b", "dependencies": { "angular": { "version": "1.4.7", @@ -83,8 +83,8 @@ }, "angular-drag-and-drop-lists": { "version": "1.4.0", - "from": "leigh-johnson/angular-drag-and-drop-lists#1.4.0", - "resolved": "git://github.com/leigh-johnson/angular-drag-and-drop-lists.git#4d32654ab7159689a7767b9be8fc85f9812ca5a8" + "from": "ansible/angular-drag-and-drop-lists#1.4.0", + "resolved": "git://github.com/ansible/angular-drag-and-drop-lists.git#4d32654ab7159689a7767b9be8fc85f9812ca5a8" }, "angular-duration-format": { "version": "1.0.1", @@ -147,8 +147,8 @@ }, "angular-scheduler": { "version": "0.1.0", - "from": "chouseknecht/angular-scheduler#0.1.0", - "resolved": "git://github.com/chouseknecht/angular-scheduler.git#784693054597b9a1c1e49efb4cf94e9054b92e66", + "from": "ansible/angular-scheduler#0.1.0", + "resolved": "git://github.com/ansible/angular-scheduler.git#784693054597b9a1c1e49efb4cf94e9054b92e66", "dependencies": { "angular-tz-extensions": { "version": "0.3.11", @@ -171,13 +171,18 @@ "version": "3.8.0", "from": "lodash@>=3.8.0 <3.9.0", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.8.0.tgz" + }, + "timezone-js": { + "version": "0.4.14", + "from": "leigh-johnson/timezone-js#0.4.14", + "resolved": "git://github.com/leigh-johnson/timezone-js.git#6937de14ce0c193961538bb5b3b12b7ef62a358f" } } }, "angular-tz-extensions": { "version": "0.3.11", - "from": "chouseknecht/angular-tz-extensions#0.3.12", - "resolved": "git://github.com/chouseknecht/angular-tz-extensions.git#938577310ff9a343eae1348aa04a3ed1a96d097f", + "from": "ansible/angular-tz-extensions#0.3.13", + "resolved": "git://github.com/ansible/angular-tz-extensions.git#33caaa9ccf5dfe29a95962c17c3c9e6b9775be35", "dependencies": { "angular": { "version": "1.4.7", @@ -188,6 +193,11 @@ "version": "3.1.1", "from": "jquery@>=3.1.0 <4.0.0", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.1.1.tgz" + }, + "timezone-js": { + "version": "0.4.14", + "from": "ansible/timezone-js#0.4.14", + "resolved": "git://github.com/ansible/timezone-js.git#6937de14ce0c193961538bb5b3b12b7ef62a358f" } } }, @@ -3643,8 +3653,8 @@ }, "ng-toast": { "version": "2.0.0", - "from": "leigh-johnson/ngToast#2.0.1", - "resolved": "git://github.com/leigh-johnson/ngToast.git#fea95bb34d27687e414619b4f72c11735d909f93" + "from": "ansible/ngToast#2.0.1", + "resolved": "git://github.com/ansible/ngToast.git#fea95bb34d27687e414619b4f72c11735d909f93" }, "node-libs-browser": { "version": "0.6.0", @@ -3710,8 +3720,8 @@ }, "nvd3": { "version": "1.7.1", - "from": "leigh-johnson/nvd3#1.7.1", - "resolved": "git://github.com/leigh-johnson/nvd3.git#a28bcd494a1df0677be7cf2ebc0578f44eb21102", + "from": "ansible/nvd3#1.7.1", + "resolved": "git://github.com/ansible/nvd3.git#a28bcd494a1df0677be7cf2ebc0578f44eb21102", "dependencies": { "d3": { "version": "3.3.13", @@ -3783,7 +3793,14 @@ "optimist": { "version": "0.6.1", "from": "optimist@>=0.6.1 <0.7.0", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz" + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "dependencies": { + "minimist": { + "version": "0.0.10", + "from": "minimist@>=0.0.1 <0.1.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" + } + } }, "optionator": { "version": "0.8.2", @@ -4745,8 +4762,8 @@ }, "timezone-js": { "version": "0.4.14", - "from": "leigh-johnson/timezone-js#0.4.14", - "resolved": "git://github.com/leigh-johnson/timezone-js.git#6937de14ce0c193961538bb5b3b12b7ef62a358f" + "from": "ansible/timezone-js", + "resolved": "git://github.com/ansible/timezone-js.git#6937de14ce0c193961538bb5b3b12b7ef62a358f" }, "tiny-lr": { "version": "0.2.1", diff --git a/awx/ui/package.json b/awx/ui/package.json index 737eaed35c..9c876a2db3 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -80,18 +80,18 @@ }, "dependencies": { "angular": "~1.4.7", - "angular-breadcrumb": "leigh-johnson/angular-breadcrumb#0.4.1", - "angular-codemirror": "chouseknecht/angular-codemirror#1.0.4", + "angular-breadcrumb": "github:ansible/angular-breadcrumb#0.4.1", + "angular-codemirror": "github:ansible/angular-codemirror#1.0.4", "angular-cookies": "^1.4.3", - "angular-drag-and-drop-lists": "leigh-johnson/angular-drag-and-drop-lists#1.4.0", + "angular-drag-and-drop-lists": "github:ansible/angular-drag-and-drop-lists#1.4.0", "angular-duration-format": "^1.0.1", "angular-gettext": "^2.3.5", "angular-md5": "^0.1.8", "angular-moment": "^0.10.1", "angular-resource": "^1.4.3", "angular-sanitize": "^1.4.3", - "angular-scheduler": "chouseknecht/angular-scheduler#0.1.0", - "angular-tz-extensions": "chouseknecht/angular-tz-extensions#0.3.12", + "angular-scheduler": "github:ansible/angular-scheduler#0.1.0", + "angular-tz-extensions": "github:ansible/angular-tz-extensions#0.3.13", "angular-ui-router": "^1.0.0-beta.3", "bootstrap": "^3.1.1", "bootstrap-datepicker": "^1.4.0", @@ -104,12 +104,14 @@ "js-yaml": "^3.2.7", "legacy-loader": "0.0.2", "lodash": "^3.8.0", - "lr-infinite-scroll": "lorenzofox3/lrInfiniteScroll", + "lr-infinite-scroll": "github:lorenzofox3/lrInfiniteScroll", "moment": "^2.10.2", - "ng-toast": "leigh-johnson/ngToast#2.0.1", - "nvd3": "leigh-johnson/nvd3#1.7.1", + "ng-toast": "github:ansible/ngToast#2.0.1", + "nvd3": "github:ansible/nvd3#1.7.1", "reconnectingwebsocket": "^1.0.0", + "rrule": "github:jkbrzt/rrule#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c", "select2": "^4.0.2", - "sprintf-js": "^1.0.3" + "sprintf-js": "^1.0.3", + "timezone-js": "github:ansible/timezone-js" } } From eebdd13aa1a2d2515fb02f019aa3cae7000cd775 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 6 Dec 2016 11:39:08 -0500 Subject: [PATCH 017/595] Updating schedules help text --- awx/main/models/schedules.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/awx/main/models/schedules.py b/awx/main/models/schedules.py index 786e788aa3..243201d377 100644 --- a/awx/main/models/schedules.py +++ b/awx/main/models/schedules.py @@ -10,6 +10,7 @@ import dateutil.rrule from django.db import models from django.db.models.query import QuerySet from django.utils.timezone import now, make_aware, get_default_timezone +from django.utils.translation import ugettext_lazy as _ # AWX from awx.main.models.base import * # noqa @@ -65,24 +66,29 @@ class Schedule(CommonModel): ) enabled = models.BooleanField( default=True, + help_text=_("Enables processing of this schedule by Tower") ) dtstart = models.DateTimeField( null=True, default=None, editable=False, + help_text=_("The first occurrence of the schedule occurs on or after this time") ) dtend = models.DateTimeField( null=True, default=None, editable=False, + help_text=_("The last occurrence of the schedule occurs before this time, aftewards the schedule expires") ) rrule = models.CharField( max_length=255, + help_text=_("A value representing the schedules iCal recurrence rule") ) next_run = models.DateTimeField( null=True, default=None, editable=False, + help_text=_("The next time that the scheduled action will run") ) extra_data = JSONField( blank=True, From 832011aa99f3b93ab3793001291e01cc97451fb4 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Tue, 6 Dec 2016 11:40:21 -0500 Subject: [PATCH 018/595] Mark additional strings for translation. --- awx/ui/client/src/controllers/Projects.js | 16 ++++----- awx/ui/client/src/controllers/Users.js | 28 +++++++-------- .../dashboard/hosts/dashboard-hosts.list.js | 12 +++---- awx/ui/client/src/forms/JobTemplates.js | 12 +++---- awx/ui/client/src/forms/Projects.js | 6 ++-- awx/ui/client/src/forms/Teams.js | 2 +- awx/ui/client/src/forms/Users.js | 10 +++--- awx/ui/client/src/forms/Workflows.js | 4 +-- awx/ui/client/src/helpers/Credentials.js | 10 +++--- .../client/src/license/license.controller.js | 4 +-- awx/ui/client/src/lists/CompletedJobs.js | 4 +-- awx/ui/client/src/lists/Inventories.js | 3 +- awx/ui/client/src/lists/Teams.js | 3 +- awx/ui/client/src/lists/Templates.js | 7 ++-- awx/ui/client/src/lists/Users.js | 2 +- .../thirdPartySignOn.service.js | 4 +-- .../src/notifications/notifications.list.js | 4 +-- awx/ui/client/src/shared/form-generator.js | 34 +++++++++++-------- 18 files changed, 82 insertions(+), 83 deletions(-) diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 429338cd5f..17076a2a73 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -281,7 +281,7 @@ export function ProjectsAdd($scope, $rootScope, $compile, $location, $log, .success(function(data) { if (!data.actions.POST) { $state.go("^"); - Alert('Permission Error', 'You do not have permission to add a project.', 'alert-info'); + Alert(i18n._('Permission Error'), i18n._('You do not have permission to add a project.'), 'alert-info'); } }); @@ -465,7 +465,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, }); $scope.project_local_paths = opts; $scope.local_path = $scope.project_local_paths[0]; - $scope.base_dir = 'You do not have access to view this property'; + $scope.base_dir = i18n._('You do not have access to view this property'); $scope.$emit('pathsReady'); } @@ -555,7 +555,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, }) .error(function (data, status) { ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), - msg: i18n._('Failed to retrieve project: ') + id + i18n._('. GET status: ') + status + msg: i18n.sprintf(i18n._('Failed to retrieve project: %s. GET status: '), id) + status }); }); }); @@ -620,7 +620,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, $state.go($state.current, {}, { reload: true }); }) .error(function(data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update project: ' + id + '. PUT status: ' + status }); + ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Failed to update project: %s. PUT status: '), id) + status }); }); }; @@ -638,7 +638,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, }) .error(function(data, status) { $('#prompt-modal').modal('hide'); - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. POST returned status: ' + status }); + ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. POST returned status: '), url) + status }); }); }; @@ -646,7 +646,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, hdr: i18n._('Delete'), body: '
' + i18n.sprintf(i18n._('Are you sure you want to remove the %s below from %s?'), title, $scope.name) + '
' + '
' + name + '
', action: action, - actionText: 'DELETE' + actionText: i18n._('DELETE') }); }; @@ -654,7 +654,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, if ($scope.scm_type) { $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; - $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; + $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? i18n._('Revision #') : i18n._('SCM Branch'); } // Dynamically update popover values @@ -690,7 +690,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, if ($scope.project_obj.scm_type === "Manual" || Empty($scope.project_obj.scm_type)) { // ignore } else if ($scope.project_obj.status === 'updating' || $scope.project_obj.status === 'running' || $scope.project_obj.status === 'pending') { - Alert('Update in Progress', i18n._('The SCM update process is running.'), 'alert-info'); + Alert(i18n._('Update in Progress'), i18n._('The SCM update process is running.'), 'alert-info'); } else { ProjectUpdate({ scope: $scope, project_id: $scope.project_obj.id }); } diff --git a/awx/ui/client/src/controllers/Users.js b/awx/ui/client/src/controllers/Users.js index 61ffb69d2e..e53f214e0f 100644 --- a/awx/ui/client/src/controllers/Users.js +++ b/awx/ui/client/src/controllers/Users.js @@ -91,17 +91,17 @@ export function UsersList($scope, $rootScope, $stateParams, }) .error(function(data, status) { ProcessErrors($scope, data, status, null, { - hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status + hdr: i18n._('Error!'), + msg: i18n.sprintf(i18n._('Call to %s failed. DELETE returned status: '), url) + status }); }); }; Prompt({ - hdr: 'Delete', - body: '
Are you sure you want to delete the user below?
' + $filter('sanitize')(name) + '
', + hdr: i18n._('Delete'), + body: '
' + i18n._('Are you sure you want to delete the user below?') + '
' + $filter('sanitize')(name) + '
', action: action, - actionText: 'DELETE' + actionText: i18n._('DELETE') }); }; } @@ -138,7 +138,7 @@ export function UsersAdd($scope, $rootScope, $stateParams, UserForm, .success(function(data) { if (!data.actions.POST) { $state.go("^"); - Alert('Permission Error', 'You do not have permission to add a user.', 'alert-info'); + Alert(i18n._('Permission Error'), i18n._('You do not have permission to add a user.'), 'alert-info'); } }); @@ -171,7 +171,7 @@ export function UsersAdd($scope, $rootScope, $stateParams, UserForm, .success(function(data) { var base = $location.path().replace(/^\//, '').split('/')[0]; if (base === 'users') { - $rootScope.flashMessage = 'New user successfully created!'; + $rootScope.flashMessage = i18n._('New user successfully created!'); $rootScope.$broadcast("EditIndicatorChange", "users", data.id); $state.go('users.edit', { user_id: data.id }, { reload: true }); } else { @@ -179,10 +179,10 @@ export function UsersAdd($scope, $rootScope, $stateParams, UserForm, } }) .error(function(data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to add new user. POST returned status: ' + status }); + ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), msg: i18n._('Failed to add new user. POST returned status: ') + status }); }); } else { - $scope.organization_name_api_error = 'A value is required'; + $scope.organization_name_api_error = i18n._('A value is required'); } } }; @@ -264,9 +264,8 @@ export function UsersEdit($scope, $rootScope, $location, }) .error(function(data, status) { ProcessErrors($scope, data, status, null, { - hdr: 'Error!', - msg: 'Failed to retrieve user: ' + - $stateParams.id + '. GET status: ' + status + hdr: i18n._('Error!'), + msg: i18n.sprintf(i18n._('Failed to retrieve user: %s. GET status: '), $stateParams.id) + status }); }); } @@ -319,9 +318,8 @@ export function UsersEdit($scope, $rootScope, $location, }) .error(function(data, status) { ProcessErrors($scope, data, status, null, { - hdr: 'Error!', - msg: 'Failed to retrieve user: ' + - $stateParams.id + '. GET status: ' + status + hdr: i18n._('Error!'), + msg: i18n.sprintf(i18n._('Failed to retrieve user: %s. GET status: '), $stateParams.id) + status }); }); } diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts.list.js b/awx/ui/client/src/dashboard/hosts/dashboard-hosts.list.js index 0c3c0adb9d..f439c0c859 100644 --- a/awx/ui/client/src/dashboard/hosts/dashboard-hosts.list.js +++ b/awx/ui/client/src/dashboard/hosts/dashboard-hosts.list.js @@ -38,7 +38,7 @@ export default [ 'i18n', function(i18n){ ngClick: 'editHost(host.id)' }, inventory_name: { - label: 'Inventory', + label: i18n._('Inventory'), sourceModel: 'inventory', sourceField: 'name', columnClass: 'col-lg-5 col-md-4 col-sm-4 hidden-xs elllipsis', @@ -46,13 +46,13 @@ export default [ 'i18n', function(i18n){ searchable: false }, enabled: { - label: 'Status', + label: i18n._('Status'), columnClass: 'List-staticColumn--toggle', type: 'toggle', ngClick: 'toggleHostEnabled(host)', nosort: true, - awToolTip: "

Indicates if a host is available and should be included in running jobs.

For hosts that are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.

", - dataTitle: 'Host Enabled', + awToolTip: "

" + i18n._("Indicates if a host is available and should be included in running jobs.") + "

" + i18n._("For hosts that are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.") + "

", + dataTitle: i18n._('Host Enabled'), } }, @@ -60,10 +60,10 @@ export default [ 'i18n', function(i18n){ columnClass: 'col-lg-2 col-md-3 col-sm-3 col-xs-4', edit: { - label: 'Edit', + label: i18n._('Edit'), ngClick: 'editHost(host.id)', icon: 'icon-edit', - awToolTip: 'Edit host', + awToolTip: i18n._('Edit host'), dataPlacement: 'top' } }, diff --git a/awx/ui/client/src/forms/JobTemplates.js b/awx/ui/client/src/forms/JobTemplates.js index 9fe64f0469..23be861723 100644 --- a/awx/ui/client/src/forms/JobTemplates.js +++ b/awx/ui/client/src/forms/JobTemplates.js @@ -20,7 +20,7 @@ export default addTitle: i18n._('New Job Template'), editTitle: '{{ name }}', name: 'job_template', - breadcrumbName: 'JOB TEMPLATE', + breadcrumbName: i18n._('JOB TEMPLATE'), basePath: 'job_templates', // the top-most node of generated state tree stateTree: 'templates', @@ -80,7 +80,7 @@ export default reqExpression: '!ask_inventory_on_launch', alwaysShowAsterisk: true }, - requiredErrorMsg: "Please select an Inventory or check the Prompt on launch option.", + requiredErrorMsg: i18n._("Please select an Inventory or check the Prompt on launch option."), column: 1, awPopOver: "

" + i18n._("Select the inventory containing the hosts you want this job to manage.") + "

", dataTitle: i18n._('Inventory'), @@ -96,7 +96,7 @@ export default project: { label: i18n._('Project'), labelAction: { - label: 'RESET', + label: i18n._('RESET'), ngClick: 'resetProjectToDefault()', 'class': "{{!(job_type.value === 'scan' && project_name !== 'Default') ? 'hidden' : ''}}", }, @@ -147,7 +147,7 @@ export default reqExpression: '!ask_credential_on_launch', alwaysShowAsterisk: true }, - requiredErrorMsg: "Please select a Machine Credential or check the Prompt on launch option.", + requiredErrorMsg: i18n._("Please select a Machine Credential or check the Prompt on launch option."), column: 1, awPopOver: "

" + i18n._("Select the credential you want the job to use when accessing the remote hosts. Choose the credential containing " + " the username and SSH key or password that Ansible will need to log into the remote hosts.") + "

", @@ -409,9 +409,9 @@ export default add: { ngClick: "$state.go('.add')", label: 'Add', - awToolTip: 'Add a permission', + awToolTip: i18n._('Add a permission'), actionClass: 'btn List-buttonSubmit', - buttonContent: '+ ADD', + buttonContent: '+ ' + i18n._('ADD'), ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' } }, diff --git a/awx/ui/client/src/forms/Projects.js b/awx/ui/client/src/forms/Projects.js index a1d65a82d5..7fef97a9ad 100644 --- a/awx/ui/client/src/forms/Projects.js +++ b/awx/ui/client/src/forms/Projects.js @@ -242,18 +242,18 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition']) fields: { username: { - label: 'User', + label: i18n._('User'), uiSref: 'users({user_id: field.id})', class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4' }, role: { - label: 'Role', + label: i18n._('Role'), type: 'role', noSort: true, class: 'col-lg-4 col-md-4 col-sm-4 col-xs-4', }, team_roles: { - label: 'Team Roles', + label: i18n._('Team Roles'), type: 'team_roles', noSort: true, class: 'col-lg-5 col-md-5 col-sm-5 col-xs-4', diff --git a/awx/ui/client/src/forms/Teams.js b/awx/ui/client/src/forms/Teams.js index a7f03a490b..c2550f5543 100644 --- a/awx/ui/client/src/forms/Teams.js +++ b/awx/ui/client/src/forms/Teams.js @@ -80,7 +80,7 @@ export default add: { // @issue https://github.com/ansible/ansible-tower/issues/3487 //ngClick: "addPermissionWithoutTeamTab", - label: 'Add', + label: i18n._('Add'), awToolTip: i18n._('Add user to team'), actionClass: 'btn List-buttonSubmit', buttonContent: '+ ' + i18n._('ADD'), diff --git a/awx/ui/client/src/forms/Users.js b/awx/ui/client/src/forms/Users.js index 5e95b6166f..20f8126a83 100644 --- a/awx/ui/client/src/forms/Users.js +++ b/awx/ui/client/src/forms/Users.js @@ -136,10 +136,10 @@ export default fields: { name: { key: true, - label: 'Name' + label: i18n._('Name') }, description: { - label: 'Description' + label: i18n._('Description') } }, //hideOnSuperuser: true // RBAC defunct @@ -157,14 +157,14 @@ export default open: false, index: false, actions: {}, - emptyListText: 'This user is not a member of any teams', + emptyListText: i18n._('This user is not a member of any teams'), fields: { name: { key: true, - label: 'Name' + label: i18n._('Name') }, description: { - label: 'Description' + label: i18n._('Description') } }, //hideOnSuperuser: true // RBAC defunct diff --git a/awx/ui/client/src/forms/Workflows.js b/awx/ui/client/src/forms/Workflows.js index d281ae0e0b..136d0692a9 100644 --- a/awx/ui/client/src/forms/Workflows.js +++ b/awx/ui/client/src/forms/Workflows.js @@ -120,10 +120,10 @@ export default actions: { add: { ngClick: "$state.go('.add')", - label: 'Add', + label: i18n._('Add'), awToolTip: 'Add a permission', actionClass: 'btn List-buttonSubmit', - buttonContent: '+ ADD', + buttonContent: '+ '+ i18n._('ADD'), ngShow: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)' } }, diff --git a/awx/ui/client/src/helpers/Credentials.js b/awx/ui/client/src/helpers/Credentials.js index 375a556be0..fb7477d61f 100644 --- a/awx/ui/client/src/helpers/Credentials.js +++ b/awx/ui/client/src/helpers/Credentials.js @@ -305,12 +305,12 @@ angular.module('CredentialsHelper', ['Utilities']) // the error there. The ssh_key_unlock field is not shown when the kind of credential is gce/azure and as a result the // error is never shown. In the future, the API will hopefully either behave or respond differently. if(status && status === 400 && data && data.ssh_key_unlock && (scope.kind.value === 'gce' || scope.kind.value === 'azure')) { - scope.ssh_key_data_api_error = "Encrypted credentials are not supported."; + scope.ssh_key_data_api_error = i18n._("Encrypted credentials are not supported."); } else { ProcessErrors(scope, data, status, form, { - hdr: 'Error!', - msg: 'Failed to create new Credential. POST status: ' + status + hdr: i18n._('Error!'), + msg: i18n._('Failed to create new Credential. POST status: ') + status }); } }); @@ -325,8 +325,8 @@ angular.module('CredentialsHelper', ['Utilities']) .error(function (data, status) { Wait('stop'); ProcessErrors(scope, data, status, form, { - hdr: 'Error!', - msg: 'Failed to update Credential. PUT status: ' + status + hdr: i18n._('Error!'), + msg: i18n._('Failed to update Credential. PUT status: ') + status }); }); } diff --git a/awx/ui/client/src/license/license.controller.js b/awx/ui/client/src/license/license.controller.js index 682f1cfb01..195178dd28 100644 --- a/awx/ui/client/src/license/license.controller.js +++ b/awx/ui/client/src/license/license.controller.js @@ -66,14 +66,14 @@ export default $scope.newLicense.file = JSON.parse(raw.result); } catch(err) { - ProcessErrors($rootScope, null, null, null, {msg: 'Invalid file format. Please upload valid JSON.'}); + ProcessErrors($rootScope, null, null, null, {msg: i18n._('Invalid file format. Please upload valid JSON.')}); } }; try { raw.readAsText(event.target.files[0]); } catch(err) { - ProcessErrors($rootScope, null, null, null, {msg: 'Invalid file format. Please upload valid JSON.'}); + ProcessErrors($rootScope, null, null, null, {msg: i18n._('Invalid file format. Please upload valid JSON.')}); } }; // HTML5 spec doesn't provide a way to customize file input css diff --git a/awx/ui/client/src/lists/CompletedJobs.js b/awx/ui/client/src/lists/CompletedJobs.js index dc219d629b..1c0436240c 100644 --- a/awx/ui/client/src/lists/CompletedJobs.js +++ b/awx/ui/client/src/lists/CompletedJobs.js @@ -72,14 +72,14 @@ export default icon: 'icon-rocket', mode: 'all', ngClick: 'relaunchJob($event, completed_job.id)', - awToolTip: 'Relaunch using the same parameters', + awToolTip: i18n._('Relaunch using the same parameters'), dataPlacement: 'top', ngShow: "!completed_job.type == 'system_job' || completed_job.summary_fields.user_capabilities.start" }, "delete": { mode: 'all', ngClick: 'deleteJob(completed_job.id)', - awToolTip: 'Delete the job', + awToolTip: i18n._('Delete the job'), dataPlacement: 'top', ngShow: 'completed_job.summary_fields.user_capabilities.delete' } diff --git a/awx/ui/client/src/lists/Inventories.js b/awx/ui/client/src/lists/Inventories.js index d3d68223cf..4ae755cfb6 100644 --- a/awx/ui/client/src/lists/Inventories.js +++ b/awx/ui/client/src/lists/Inventories.js @@ -15,8 +15,7 @@ export default selectTitle: i18n._('Add Inventories'), editTitle: i18n._('Inventories'), listTitle: i18n._('Inventories'), - selectInstructions: "Click on a row to select it, and click Finished when done. Click the " + - "button to create a new inventory.", + selectInstructions: i18n.sprintf(i18n._("Click on a row to select it, and click Finished when done. Click the %s button to create a new inventory."), " "), index: false, hover: true, basePath: 'inventory', diff --git a/awx/ui/client/src/lists/Teams.js b/awx/ui/client/src/lists/Teams.js index b9bb151a1a..0048be52d0 100644 --- a/awx/ui/client/src/lists/Teams.js +++ b/awx/ui/client/src/lists/Teams.js @@ -15,8 +15,7 @@ export default selectTitle: i18n._('Add Team'), editTitle: i18n._('Teams'), listTitle: i18n._('Teams'), - selectInstructions: "Click on a row to select it, and click Finished when done. Click the " + - "button to create a new team.", + selectInstructions: i18n.sprintf(i18n._("Click on a row to select it, and click Finished when done. Click the %s button to create a new team."), " "), index: false, hover: true, diff --git a/awx/ui/client/src/lists/Templates.js b/awx/ui/client/src/lists/Templates.js index 10a195f795..7db934ffd3 100644 --- a/awx/ui/client/src/lists/Templates.js +++ b/awx/ui/client/src/lists/Templates.js @@ -16,8 +16,7 @@ export default selectTitle: i18n._('Template'), editTitle: i18n._('Templates'), listTitle: i18n._('Templates'), - selectInstructions: "Click on a row to select it, and click Finished when done. Use the " + - "button to create a new job template.", + selectInstructions: i18n.sprintf(i18n._("Click on a row to select it, and click Finished when done. Use the %s button to create a new job template."), " "), index: false, hover: true, @@ -62,12 +61,12 @@ export default buttonContent: i18n._('ADD'), options: [ { - optionContent: 'Job Template', + optionContent: i18n._('Job Template'), optionSref: 'templates.addJobTemplate', ngShow: 'canAddJobTemplate' }, { - optionContent: 'Workflow Job Template', + optionContent: i18n._('Workflow Job Template'), optionSref: 'templates.addWorkflowJobTemplate', ngShow: 'canAddWorkflowJobTemplate' } diff --git a/awx/ui/client/src/lists/Users.js b/awx/ui/client/src/lists/Users.js index 3efb0f7ce3..bfff119616 100644 --- a/awx/ui/client/src/lists/Users.js +++ b/awx/ui/client/src/lists/Users.js @@ -49,7 +49,7 @@ export default actions: { add: { - label: 'Create New', + label: i18n._('Create New'), mode: 'all', // One of: edit, select, all ngClick: 'addUser()', basePaths: ['organizations', 'users'], // base path must be in list, or action not available diff --git a/awx/ui/client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js b/awx/ui/client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js index 7ab1980495..ab35628d12 100644 --- a/awx/ui/client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js +++ b/awx/ui/client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js @@ -116,8 +116,8 @@ return {"options": options, "error": error}; }) .catch(function (data) { - ProcessErrors(scope, data.data, data.status, null, { hdr: 'Error!', - msg: 'Failed to get third-party login types. Returned status: ' + data.status }); + ProcessErrors(scope, data.data, data.status, null, { hdr: i18n._('Error!'), + msg: i18n._('Failed to get third-party login types. Returned status: ') + data.status }); }); }; }]; diff --git a/awx/ui/client/src/notifications/notifications.list.js b/awx/ui/client/src/notifications/notifications.list.js index 5509dfebfe..7a7a345c96 100644 --- a/awx/ui/client/src/notifications/notifications.list.js +++ b/awx/ui/client/src/notifications/notifications.list.js @@ -18,7 +18,7 @@ export default ['i18n', function(i18n){ iterator: 'notification', index: false, hover: false, - emptyListText: "This list is populated by notification templates added from the Notifications section", + emptyListText: i18n.sprintf(i18n._("This list is populated by notification templates added from the %sNotifications%s section"), " ", " "), basePath: 'notification_templates', fields: { name: { @@ -60,7 +60,7 @@ export default ['i18n', function(i18n){ }, actions: { add: { - label: 'Add Notification', + label: i18n._('Add Notification'), mode: 'all', // One of: edit, select, all ngClick: 'addNotificationTemplate()', awToolTip: i18n._('Create a new notification template'), diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 5866a464ba..3c931b64d2 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -542,9 +542,9 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += (field.flag) ? field.flag : "enabled"; html += "\}' aw-tool-tip='" + field.awToolTip + "' data-placement='" + field.dataPlacement + "' data-tip-watch='" + field.dataTipWatch + "'>
ON
" + i18n._("ON") + "
OFF
"; + html += "' class='ScheduleToggle-switch' ng-click='" + field.ngClick + "'>" + i18n._("OFF") + ""; } return html; }, @@ -1041,7 +1041,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat // Add error messages if (field.required) { html += "
" + (field.requiredErrorMsg ? field.requiredErrorMsg : "Please enter a value.") + "
\n"; + this.form.name + '_form.' + fld + ".$error.required\">" + (field.requiredErrorMsg ? field.requiredErrorMsg : i18n._("Please enter a value.")) + "\n"; } html += "
\n"; html += "\n"; @@ -1108,7 +1108,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat // Add error messages if (field.required || field.awRequiredWhen) { html += "
" + (field.requiredErrorMsg ? field.requiredErrorMsg : "Please select a value."); + this.form.name + '_form.' + fld + ".$error.required\">" + (field.requiredErrorMsg ? field.requiredErrorMsg : i18n._("Please select a value.")); if (field.includePlaybookNotFoundError) { html += " Playbook {{ job_template_obj.playbook }} not found for project.\n"; } @@ -1177,15 +1177,19 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat // Add error messages if (field.required) { html += "
" + (field.requiredErrorMsg ? field.requiredErrorMsg : "Please select a value.") + "
\n"; + this.form.name + '_form.' + fld + ".$error.required\">" + (field.requiredErrorMsg ? field.requiredErrorMsg : i18n._("Please select a value.")) + "
\n"; } if (field.integer) { - html += "
Please enter a number.
\n"; + html += "
" + i18n._("Please enter a number.") + "
\n"; } if (field.min !== undefined || field.max !== undefined) { html += "
Please enter a number greater than " + field.min; - html += (field.max !== undefined) ? " and less than " + field.max + "." : "."; + this.form.name + '_form.' + fld + ".$error.max\">"; + if (field.max !== undefined) { + html += i18n.sprintf(i18n._("Please enter a number greater than %d and less than %d."), field.min, field.max) + } else { + html += i18n.sprintf(i18n._("Please enter a number greater than %d.", field.min) + } html += "
\n"; } html += "
\n"; @@ -1210,14 +1214,14 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat // Add error messages if (field.required) { html += "
" + (field.requiredErrorMsg ? field.requiredErrorMsg : "Please select at least one value.") + "
\n"; + this.form.name + '_form.' + fld + ".$error.required\">" + (field.requiredErrorMsg ? field.requiredErrorMsg : i18n._("Please select at least one value.")) + "\n"; } if (field.integer) { - html += "
Please select a number.
\n"; + html += "
" + i18n._("Please select a number.") + "
\n"; } if (field.min || field.max) { html += "
Please select a number between " + field.min + " and " + + this.form.name + '_form.' + fld + ".$error.max\">" + i18n._("Please select a number between ") + field.min + i18n._(" and ") + field.max + "
\n"; } html += "
\n"; @@ -1291,7 +1295,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat if (field.required || field.awRequiredWhen) { html += "
Please select a value.
\n"; + this.form.name + '_form.' + fld + ".$error.required\">" + i18n._("Please select a value.)" + "\n"; } html += "
\n"; @@ -1396,13 +1400,13 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat this.form.name + '_form.' + field.sourceModel + '_' + field.sourceField + ".$dirty && " + this.form.name + '_form.' + field.sourceModel + '_' + field.sourceField + - ".$error.required\">" + (field.requiredErrorMsg ? field.requiredErrorMsg : "Please select a value.") + "\n"; + ".$error.required\">" + (field.requiredErrorMsg ? field.requiredErrorMsg : i18n._("Please select a value.")) + "\n"; } html += "
That value was not found. Please enter or select a valid value.
\n"; + ".$error.awlookup\">" + i18n._("That value was not found. Please enter or select a valid value.") + "\n"; html += "
\n"; html += "\n"; @@ -1858,7 +1862,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat `; // Show the "no items" box when loading is done and the user isn't actively searching and there are no results - var emptyListText = (collection.emptyListText) ? collection.emptyListText : "PLEASE ADD ITEMS TO THIS LIST"; + var emptyListText = (collection.emptyListText) ? collection.emptyListText : i18n._("PLEASE ADD ITEMS TO THIS LIST"); html += `
`; html += `
${emptyListText}
`; html += '
'; From d7ffdf7020f2b7d98d492c48ae9585782d52c4eb Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 6 Dec 2016 12:03:44 -0500 Subject: [PATCH 019/595] Updating project help text --- awx/main/models/projects.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 1b56ae72a3..2d045cee6f 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -78,12 +78,14 @@ class ProjectOptions(models.Model): blank=True, default='', verbose_name=_('SCM Type'), + help_text=_("Specifies the source control system used to store the project."), ) scm_url = models.CharField( max_length=1024, blank=True, default='', verbose_name=_('SCM URL'), + help_text=_("The location where the project is stored."), ) scm_branch = models.CharField( max_length=256, @@ -94,9 +96,11 @@ class ProjectOptions(models.Model): ) scm_clean = models.BooleanField( default=False, + help_text=_('Discard any local changes before syncing the project.'), ) scm_delete_on_update = models.BooleanField( default=False, + help_text=_('Delete the project before syncing.'), ) credential = models.ForeignKey( 'Credential', @@ -109,6 +113,7 @@ class ProjectOptions(models.Model): timeout = models.IntegerField( blank=True, default=0, + help_text=_("The amount of time to run before the task is canceled"), ) def clean_scm_type(self): @@ -221,10 +226,13 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): ) scm_update_on_launch = models.BooleanField( default=False, + help_text=_('Update the project when a job is launched that uses the project.'), ) scm_update_cache_timeout = models.PositiveIntegerField( default=0, blank=True, + help_text=_('The number of seconds after the last project update ran that a new' + 'project update will be launched as a job dependency.'), ) scm_revision = models.CharField( From f4bf4e59710fbc4c7cc8025aa757f489b039a743 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Tue, 6 Dec 2016 12:08:48 -0500 Subject: [PATCH 020/595] Update --- awx/ui/po/ansible-tower-ui.pot | 606 +++++++++++++++++++-------------- 1 file changed, 357 insertions(+), 249 deletions(-) diff --git a/awx/ui/po/ansible-tower-ui.pot b/awx/ui/po/ansible-tower-ui.pot index 5a0b8f2bd6..62433d3bc7 100644 --- a/awx/ui/po/ansible-tower-ui.pot +++ b/awx/ui/po/ansible-tower-ui.pot @@ -9,12 +9,12 @@ msgid "%s or %s" msgstr "" #: client/src/controllers/Projects.js:397 -#: client/src/controllers/Projects.js:678 +#: client/src/controllers/Projects.js:679 msgid "%sNote:%s Mercurial does not support password authentication for SSH. Do not put the username and key in the URL. If using Bitbucket and SSH, do not supply your Bitbucket username." msgstr "" #: client/src/controllers/Projects.js:384 -#: client/src/controllers/Projects.js:665 +#: client/src/controllers/Projects.js:666 msgid "%sNote:%s When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, do not enter a username (other than git). Additionally, GitHub and Bitbucket do not support password authentication when using SSH. GIT read only protocol (git://) does not use username or password information." msgstr "" @@ -26,21 +26,23 @@ msgstr "" msgid "+ ADD" msgstr "" -#: client/src/controllers/Projects.js:557 -msgid ". GET status:" +#: client/src/controllers/Users.js:185 +msgid "A value is required" msgstr "" #: client/src/forms/Credentials.js:442 #: client/src/forms/Inventories.js:153 +#: client/src/forms/JobTemplates.js:414 #: client/src/forms/Organizations.js:75 #: client/src/forms/Projects.js:238 #: client/src/forms/Teams.js:86 +#: client/src/forms/Workflows.js:126 #: client/src/inventory-scripts/inventory-scripts.list.js:45 #: client/src/lists/Credentials.js:59 -#: client/src/lists/Inventories.js:69 -#: client/src/lists/Projects.js:61 -#: client/src/lists/Teams.js:51 -#: client/src/lists/Templates.js:63 +#: client/src/lists/Inventories.js:68 +#: client/src/lists/Projects.js:67 +#: client/src/lists/Teams.js:50 +#: client/src/lists/Templates.js:61 #: client/src/lists/Users.js:58 #: client/src/notifications/notificationTemplates.list.js:52 msgid "ADD" @@ -84,13 +86,15 @@ msgid "Actions" msgstr "" #: client/src/dashboard/lists/job-templates/job-templates-list.partial.html:17 -#: client/src/lists/Templates.js:42 +#: client/src/lists/Templates.js:40 msgid "Activity" msgstr "" #: client/src/forms/Inventories.js:104 #: client/src/forms/Inventories.js:150 #: client/src/forms/Organizations.js:72 +#: client/src/forms/Teams.js:83 +#: client/src/forms/Workflows.js:123 msgid "Add" msgstr "" @@ -106,12 +110,16 @@ msgstr "" msgid "Add Inventories" msgstr "" +#: client/src/notifications/notifications.list.js:63 +msgid "Add Notification" +msgstr "" + #: client/src/lists/Projects.js:15 msgid "Add Project" msgstr "" -#: client/src/forms/Workflows.js:161 -#: client/src/shared/form-generator.js:1715 +#: client/src/forms/JobTemplates.js:459 +#: client/src/forms/Workflows.js:171 msgid "Add Survey" msgstr "" @@ -125,6 +133,7 @@ msgstr "" #: client/src/forms/Credentials.js:440 #: client/src/forms/Inventories.js:151 +#: client/src/forms/JobTemplates.js:412 #: client/src/forms/Organizations.js:73 #: client/src/forms/Projects.js:236 msgid "Add a permission" @@ -138,10 +147,6 @@ msgstr "" msgid "Add user to team" msgstr "" -#: client/src/shared/form-generator.js:1462 -msgid "Admin" -msgstr "" - #: client/src/dashboard/graphs/dashboard-graphs.partial.html:37 #: client/src/dashboard/graphs/dashboard-graphs.partial.html:43 #: client/src/dashboard/graphs/dashboard-graphs.partial.html:65 @@ -153,8 +158,8 @@ msgstr "" msgid "All Jobs" msgstr "" -#: client/src/forms/JobTemplates.js:298 -#: client/src/forms/JobTemplates.js:305 +#: client/src/forms/JobTemplates.js:299 +#: client/src/forms/JobTemplates.js:306 msgid "Allow Provisioning Callbacks" msgstr "" @@ -174,7 +179,11 @@ msgstr "" msgid "Are you sure you want to delete the project below?" msgstr "" -#: client/src/controllers/Projects.js:646 +#: client/src/controllers/Users.js:102 +msgid "Are you sure you want to delete the user below?" +msgstr "" + +#: client/src/controllers/Projects.js:647 msgid "Are you sure you want to remove the %s below from %s?" msgstr "" @@ -185,10 +194,6 @@ msgstr "" msgid "Ask at runtime?" msgstr "" -#: client/src/shared/form-generator.js:1464 -msgid "Auditor" -msgstr "" - #: client/src/forms/Credentials.js:73 msgid "Authentication for network device access. This can include SSH keys, usernames, passwords, and authorize information. Network credentials are used when submitting jobs to run playbooks against network devices." msgstr "" @@ -209,7 +214,7 @@ msgstr "" msgid "Base path used for locating playbooks. Directories found inside this path will be listed in the playbook directory drop-down. Together the base path and selected playbook directory provide the full path used to locate playbooks." msgstr "" -#: client/src/forms/JobTemplates.js:292 +#: client/src/forms/JobTemplates.js:293 msgid "Become Privilege Escalation" msgstr "" @@ -217,7 +222,7 @@ msgstr "" msgid "Browse" msgstr "" -#: client/src/app.js:315 +#: client/src/app.js:317 msgid "CREDENTIALS" msgstr "" @@ -230,6 +235,7 @@ msgid "Cache Timeout%s (seconds)%s" msgstr "" #: client/src/controllers/Projects.js:156 +#: client/src/controllers/Users.js:95 msgid "Call to %s failed. DELETE returned status:" msgstr "" @@ -238,6 +244,10 @@ msgstr "" msgid "Call to %s failed. GET status:" msgstr "" +#: client/src/controllers/Projects.js:641 +msgid "Call to %s failed. POST returned status:" +msgstr "" + #: client/src/controllers/Projects.js:180 msgid "Call to %s failed. POST status:" msgstr "" @@ -246,15 +256,11 @@ msgstr "" msgid "Call to get project failed. GET status:" msgstr "" -#: client/src/shared/form-generator.js:1703 -msgid "Cancel" -msgstr "" - #: client/src/controllers/Projects.js:196 msgid "Cancel Not Allowed" msgstr "" -#: client/src/lists/Projects.js:115 +#: client/src/lists/Projects.js:121 msgid "Cancel the SCM update" msgstr "" @@ -262,10 +268,6 @@ msgstr "" msgid "Canceled. Click for details" msgstr "" -#: client/src/shared/form-generator.js:1100 -msgid "Choose a %s" -msgstr "" - #: client/src/license/license.partial.html:97 msgid "Choose your license file, agree to the End User License Agreement, and click submit." msgstr "" @@ -274,6 +276,18 @@ msgstr "" msgid "Clean" msgstr "" +#: client/src/lists/Inventories.js:18 +msgid "Click on a row to select it, and click Finished when done. Click the %s button to create a new inventory." +msgstr "" + +#: client/src/lists/Teams.js:18 +msgid "Click on a row to select it, and click Finished when done. Click the %s button to create a new team." +msgstr "" + +#: client/src/lists/Templates.js:19 +msgid "Click on a row to select it, and click Finished when done. Use the %s button to create a new job template." +msgstr "" + #: client/src/forms/Credentials.js:319 msgid "Client ID" msgstr "" @@ -286,12 +300,8 @@ msgstr "" msgid "Client Secret" msgstr "" -#: client/src/shared/form-generator.js:1707 -msgid "Close" -msgstr "" - -#: client/src/forms/JobTemplates.js:163 -#: client/src/forms/JobTemplates.js:175 +#: client/src/forms/JobTemplates.js:164 +#: client/src/forms/JobTemplates.js:176 msgid "Cloud Credential" msgstr "" @@ -315,22 +325,22 @@ msgstr "" msgid "Confirm Password" msgstr "" -#: client/src/forms/JobTemplates.js:254 -#: client/src/forms/JobTemplates.js:272 +#: client/src/forms/JobTemplates.js:255 +#: client/src/forms/JobTemplates.js:273 #: client/src/forms/WorkflowMaker.js:141 #: client/src/forms/WorkflowMaker.js:156 msgid "Consult the Ansible documentation for further details on the usage of tags." msgstr "" -#: client/src/forms/JobTemplates.js:240 +#: client/src/forms/JobTemplates.js:241 msgid "Control the level of output ansible will produce as the playbook executes." msgstr "" -#: client/src/lists/Templates.js:101 +#: client/src/lists/Templates.js:99 msgid "Copy" msgstr "" -#: client/src/lists/Templates.js:104 +#: client/src/lists/Templates.js:102 msgid "Copy template" msgstr "" @@ -338,6 +348,10 @@ msgstr "" msgid "Create Credential" msgstr "" +#: client/src/lists/Users.js:52 +msgid "Create New" +msgstr "" + #: client/src/lists/Credentials.js:57 msgid "Create a new credential" msgstr "" @@ -347,7 +361,7 @@ msgstr "" msgid "Create a new custom inventory" msgstr "" -#: client/src/lists/Inventories.js:67 +#: client/src/lists/Inventories.js:66 msgid "Create a new inventory" msgstr "" @@ -359,15 +373,15 @@ msgstr "" msgid "Create a new organization" msgstr "" -#: client/src/lists/Projects.js:59 +#: client/src/lists/Projects.js:65 msgid "Create a new project" msgstr "" -#: client/src/lists/Teams.js:49 +#: client/src/lists/Teams.js:48 msgid "Create a new team" msgstr "" -#: client/src/lists/Templates.js:61 +#: client/src/lists/Templates.js:59 msgid "Create a new template" msgstr "" @@ -383,7 +397,7 @@ msgstr "" msgid "Create templates for sending notifications with Email, HipChat, Slack, and SMS." msgstr "" -#: client/src/forms/JobTemplates.js:153 +#: client/src/forms/JobTemplates.js:154 #: client/src/forms/WorkflowMaker.js:60 #: client/src/forms/WorkflowMaker.js:69 msgid "Credential" @@ -400,17 +414,23 @@ msgstr "" msgid "Custom Script" msgstr "" -#: client/src/app.js:403 +#: client/src/app.js:405 msgid "DASHBOARD" msgstr "" +#: client/src/controllers/Projects.js:649 +#: client/src/controllers/Users.js:104 +msgid "DELETE" +msgstr "" + #: client/src/controllers/Projects.js:161 -#: client/src/controllers/Projects.js:645 +#: client/src/controllers/Projects.js:646 +#: client/src/controllers/Users.js:101 #: client/src/inventory-scripts/inventory-scripts.list.js:74 #: client/src/lists/Credentials.js:90 -#: client/src/lists/Inventories.js:93 -#: client/src/lists/Teams.js:78 -#: client/src/lists/Templates.js:125 +#: client/src/lists/Inventories.js:92 +#: client/src/lists/Teams.js:77 +#: client/src/lists/Templates.js:123 #: client/src/lists/Users.js:87 #: client/src/notifications/notificationTemplates.list.js:89 msgid "Delete" @@ -420,7 +440,7 @@ msgstr "" msgid "Delete credential" msgstr "" -#: client/src/lists/Inventories.js:95 +#: client/src/lists/Inventories.js:94 msgid "Delete inventory" msgstr "" @@ -436,19 +456,23 @@ msgstr "" msgid "Delete on Update" msgstr "" -#: client/src/lists/Teams.js:82 +#: client/src/lists/Teams.js:81 msgid "Delete team" msgstr "" -#: client/src/lists/Templates.js:128 +#: client/src/lists/Templates.js:126 msgid "Delete template" msgstr "" +#: client/src/lists/CompletedJobs.js:82 +msgid "Delete the job" +msgstr "" + #: client/src/forms/Projects.js:164 msgid "Delete the local repository in its entirety prior to performing an update." msgstr "" -#: client/src/lists/Projects.js:109 +#: client/src/lists/Projects.js:115 msgid "Delete the project" msgstr "" @@ -466,17 +490,19 @@ msgstr "" #: client/src/forms/Credentials.js:41 #: client/src/forms/Inventories.js:37 -#: client/src/forms/JobTemplates.js:41 +#: client/src/forms/JobTemplates.js:42 #: client/src/forms/Organizations.js:33 #: client/src/forms/Projects.js:38 #: client/src/forms/Teams.js:34 -#: client/src/forms/Workflows.js:38 +#: client/src/forms/Users.js:142 +#: client/src/forms/Users.js:167 +#: client/src/forms/Workflows.js:40 #: client/src/inventory-scripts/inventory-scripts.form.js:32 #: client/src/inventory-scripts/inventory-scripts.list.js:25 #: client/src/lists/Credentials.js:34 #: client/src/lists/PortalJobTemplates.js:29 -#: client/src/lists/Teams.js:31 -#: client/src/lists/Templates.js:38 +#: client/src/lists/Teams.js:30 +#: client/src/lists/Templates.js:36 #: client/src/notifications/notificationTemplates.form.js:36 msgid "Description" msgstr "" @@ -499,7 +525,6 @@ msgid "Destination SMS Number" msgstr "" #: client/src/license/license.partial.html:5 -#: client/src/shared/form-generator.js:1493 msgid "Details" msgstr "" @@ -524,19 +549,20 @@ msgstr "" msgid "Each time a job runs using this project, perform an update to the local repository prior to starting the job." msgstr "" +#: client/src/dashboard/hosts/dashboard-hosts.list.js:63 #: client/src/inventory-scripts/inventory-scripts.list.js:57 #: client/src/lists/Credentials.js:71 -#: client/src/lists/Inventories.js:79 -#: client/src/lists/Teams.js:61 -#: client/src/lists/Templates.js:109 +#: client/src/lists/Inventories.js:78 +#: client/src/lists/Teams.js:60 +#: client/src/lists/Templates.js:107 #: client/src/lists/Users.js:68 #: client/src/notifications/notificationTemplates.list.js:63 #: client/src/notifications/notificationTemplates.list.js:72 msgid "Edit" msgstr "" -#: client/src/forms/Workflows.js:168 -#: client/src/shared/form-generator.js:1719 +#: client/src/forms/JobTemplates.js:466 +#: client/src/forms/Workflows.js:178 msgid "Edit Survey" msgstr "" @@ -544,7 +570,11 @@ msgstr "" msgid "Edit credential" msgstr "" -#: client/src/lists/Inventories.js:81 +#: client/src/dashboard/hosts/dashboard-hosts.list.js:66 +msgid "Edit host" +msgstr "" + +#: client/src/lists/Inventories.js:80 msgid "Edit inventory" msgstr "" @@ -556,15 +586,15 @@ msgstr "" msgid "Edit notification" msgstr "" -#: client/src/lists/Teams.js:65 +#: client/src/lists/Teams.js:64 msgid "Edit team" msgstr "" -#: client/src/lists/Templates.js:111 +#: client/src/lists/Templates.js:109 msgid "Edit template" msgstr "" -#: client/src/lists/Projects.js:96 +#: client/src/lists/Projects.js:102 msgid "Edit the project" msgstr "" @@ -585,14 +615,18 @@ msgstr "" msgid "Email" msgstr "" -#: client/src/forms/JobTemplates.js:287 +#: client/src/forms/JobTemplates.js:288 msgid "Enable Privilege Escalation" msgstr "" -#: client/src/forms/JobTemplates.js:302 +#: client/src/forms/JobTemplates.js:303 msgid "Enables creation of a provisioning callback URL. Using the URL a host can contact Tower and request a configuration update using this job template." msgstr "" +#: client/src/helpers/Credentials.js:308 +msgid "Encrypted credentials are not supported." +msgstr "" + #: client/src/license/license.partial.html:108 msgid "End User License Agreement" msgstr "" @@ -620,22 +654,31 @@ msgstr "" #: client/src/controllers/Projects.js:216 #: client/src/controllers/Projects.js:225 #: client/src/controllers/Projects.js:363 -#: client/src/controllers/Projects.js:556 +#: client/src/controllers/Projects.js:557 +#: client/src/controllers/Projects.js:623 +#: client/src/controllers/Projects.js:641 +#: client/src/controllers/Users.js:182 +#: client/src/controllers/Users.js:267 +#: client/src/controllers/Users.js:321 +#: client/src/controllers/Users.js:94 +#: client/src/helpers/Credentials.js:312 +#: client/src/helpers/Credentials.js:328 +#: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:119 msgid "Error!" msgstr "" #: client/src/controllers/Projects.js:381 -#: client/src/controllers/Projects.js:663 +#: client/src/controllers/Projects.js:664 msgid "Example URLs for GIT SCM include:" msgstr "" #: client/src/controllers/Projects.js:394 -#: client/src/controllers/Projects.js:675 +#: client/src/controllers/Projects.js:676 msgid "Example URLs for Mercurial SCM include:" msgstr "" #: client/src/controllers/Projects.js:389 -#: client/src/controllers/Projects.js:670 +#: client/src/controllers/Projects.js:671 msgid "Example URLs for Subversion SCM include:" msgstr "" @@ -643,10 +686,10 @@ msgstr "" msgid "Expires On" msgstr "" -#: client/src/forms/JobTemplates.js:351 -#: client/src/forms/JobTemplates.js:363 -#: client/src/forms/Workflows.js:69 -#: client/src/forms/Workflows.js:81 +#: client/src/forms/JobTemplates.js:352 +#: client/src/forms/JobTemplates.js:364 +#: client/src/forms/Workflows.js:71 +#: client/src/forms/Workflows.js:83 msgid "Extra Variables" msgstr "" @@ -662,12 +705,37 @@ msgstr "" msgid "Failed Hosts" msgstr "" +#: client/src/controllers/Users.js:182 +msgid "Failed to add new user. POST returned status:" +msgstr "" + +#: client/src/helpers/Credentials.js:313 +msgid "Failed to create new Credential. POST status:" +msgstr "" + #: client/src/controllers/Projects.js:364 msgid "Failed to create new project. POST returned status:" msgstr "" -#: client/src/controllers/Projects.js:557 -msgid "Failed to retrieve project:" +#: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:120 +msgid "Failed to get third-party login types. Returned status:" +msgstr "" + +#: client/src/controllers/Projects.js:558 +msgid "Failed to retrieve project: %s. GET status:" +msgstr "" + +#: client/src/controllers/Users.js:268 +#: client/src/controllers/Users.js:322 +msgid "Failed to retrieve user: %s. GET status:" +msgstr "" + +#: client/src/helpers/Credentials.js:329 +msgid "Failed to update Credential. PUT status:" +msgstr "" + +#: client/src/controllers/Projects.js:623 +msgid "Failed to update project: %s. PUT status:" msgstr "" #: client/src/notifications/notifications.list.js:49 @@ -697,13 +765,17 @@ msgstr "" msgid "For example:" msgstr "" -#: client/src/forms/JobTemplates.js:222 +#: client/src/dashboard/hosts/dashboard-hosts.list.js:54 +msgid "For hosts that are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process." +msgstr "" + +#: client/src/forms/JobTemplates.js:223 #: client/src/forms/WorkflowMaker.js:125 msgid "For more information and examples see %sthe Patterns topic at docs.ansible.com%s." msgstr "" -#: client/src/forms/JobTemplates.js:198 -#: client/src/forms/JobTemplates.js:211 +#: client/src/forms/JobTemplates.js:199 +#: client/src/forms/JobTemplates.js:212 msgid "Forks" msgstr "" @@ -732,11 +804,15 @@ msgstr "" msgid "Host (Authentication URL)" msgstr "" -#: client/src/forms/JobTemplates.js:325 -#: client/src/forms/JobTemplates.js:334 +#: client/src/forms/JobTemplates.js:326 +#: client/src/forms/JobTemplates.js:335 msgid "Host Config Key" msgstr "" +#: client/src/dashboard/hosts/dashboard-hosts.list.js:55 +msgid "Host Enabled" +msgstr "" + #: client/src/dashboard/counts/dashboard-counts.directive.js:39 msgid "Hosts" msgstr "" @@ -778,7 +854,7 @@ msgstr "" msgid "IRC Server Port" msgstr "" -#: client/src/forms/JobTemplates.js:290 +#: client/src/forms/JobTemplates.js:291 msgid "If enabled, run this playbook as an administrator. This is the equivalent of passing the %s option to the %s command." msgstr "" @@ -790,7 +866,11 @@ msgstr "" msgid "If you are ready to upgrade, please contact us by clicking the button below" msgstr "" -#: client/src/forms/JobTemplates.js:57 +#: client/src/dashboard/hosts/dashboard-hosts.list.js:54 +msgid "Indicates if a host is available and should be included in running jobs." +msgstr "" + +#: client/src/forms/JobTemplates.js:58 msgid "Instead, %s will check playbook syntax, test environment setup and report problems." msgstr "" @@ -798,7 +878,12 @@ msgstr "" msgid "Invalid License" msgstr "" -#: client/src/login/loginModal/loginModal.partial.html:30 +#: client/src/license/license.controller.js:69 +#: client/src/license/license.controller.js:76 +msgid "Invalid file format. Please upload valid JSON." +msgstr "" + +#: client/src/login/loginModal/loginModal.partial.html:34 msgid "Invalid username and/or password. Please try again." msgstr "" @@ -808,8 +893,9 @@ msgstr "" msgid "Inventories" msgstr "" -#: client/src/forms/JobTemplates.js:72 -#: client/src/forms/JobTemplates.js:85 +#: client/src/dashboard/hosts/dashboard-hosts.list.js:41 +#: client/src/forms/JobTemplates.js:73 +#: client/src/forms/JobTemplates.js:86 #: client/src/forms/WorkflowMaker.js:79 #: client/src/forms/WorkflowMaker.js:89 msgid "Inventory" @@ -836,28 +922,36 @@ msgstr "" msgid "JOB STATUS" msgstr "" -#: client/src/app.js:423 +#: client/src/forms/JobTemplates.js:23 +msgid "JOB TEMPLATE" +msgstr "" + +#: client/src/app.js:425 #: client/src/dashboard/graphs/job-status/job-status-graph.directive.js:113 #: client/src/main-menu/main-menu.partial.html:122 #: client/src/main-menu/main-menu.partial.html:43 msgid "JOBS" msgstr "" -#: client/src/forms/JobTemplates.js:247 -#: client/src/forms/JobTemplates.js:255 +#: client/src/forms/JobTemplates.js:248 +#: client/src/forms/JobTemplates.js:256 #: client/src/forms/WorkflowMaker.js:134 #: client/src/forms/WorkflowMaker.js:142 msgid "Job Tags" msgstr "" +#: client/src/lists/Templates.js:64 +msgid "Job Template" +msgstr "" + #: client/src/lists/PortalJobTemplates.js:15 #: client/src/lists/PortalJobTemplates.js:16 msgid "Job Templates" msgstr "" #: client/src/dashboard/graphs/dashboard-graphs.partial.html:32 -#: client/src/forms/JobTemplates.js:47 -#: client/src/forms/JobTemplates.js:61 +#: client/src/forms/JobTemplates.js:48 +#: client/src/forms/JobTemplates.js:62 #: client/src/forms/WorkflowMaker.js:110 #: client/src/forms/WorkflowMaker.js:99 msgid "Job Type" @@ -877,11 +971,11 @@ msgstr "" msgid "Label to be shown with notification" msgstr "" -#: client/src/forms/JobTemplates.js:339 -#: client/src/forms/JobTemplates.js:344 -#: client/src/forms/Workflows.js:57 -#: client/src/forms/Workflows.js:62 -#: client/src/lists/Templates.js:49 +#: client/src/forms/JobTemplates.js:340 +#: client/src/forms/JobTemplates.js:345 +#: client/src/forms/Workflows.js:59 +#: client/src/forms/Workflows.js:64 +#: client/src/lists/Templates.js:47 msgid "Labels" msgstr "" @@ -890,13 +984,12 @@ msgstr "" msgid "Last Name" msgstr "" -#: client/src/lists/Projects.js:47 +#: client/src/lists/Projects.js:53 msgid "Last Updated" msgstr "" #: client/src/lists/PortalJobTemplates.js:39 -#: client/src/lists/Templates.js:85 -#: client/src/shared/form-generator.js:1711 +#: client/src/lists/Templates.js:83 msgid "Launch" msgstr "" @@ -925,29 +1018,25 @@ msgstr "" msgid "License Type" msgstr "" -#: client/src/forms/JobTemplates.js:217 -#: client/src/forms/JobTemplates.js:224 +#: client/src/forms/JobTemplates.js:218 +#: client/src/forms/JobTemplates.js:225 #: client/src/forms/WorkflowMaker.js:120 #: client/src/forms/WorkflowMaker.js:127 msgid "Limit" msgstr "" -#: client/src/shared/socket/socket.service.js:170 +#: client/src/shared/socket/socket.service.js:176 msgid "Live events: attempting to connect to the Tower server." msgstr "" -#: client/src/shared/socket/socket.service.js:174 +#: client/src/shared/socket/socket.service.js:180 msgid "Live events: connected. Pages containing job status information will automatically update in real-time." msgstr "" -#: client/src/shared/socket/socket.service.js:178 +#: client/src/shared/socket/socket.service.js:184 msgid "Live events: error connecting to the Tower server." msgstr "" -#: client/src/shared/form-generator.js:1969 -msgid "Loading..." -msgstr "" - #: client/src/main-menu/main-menu.partial.html:188 msgid "Log Out" msgstr "" @@ -960,7 +1049,7 @@ msgstr "" msgid "Machine" msgstr "" -#: client/src/forms/JobTemplates.js:136 +#: client/src/forms/JobTemplates.js:137 msgid "Machine Credential" msgstr "" @@ -986,7 +1075,7 @@ msgstr "" msgid "Manual projects do not require an SCM update" msgstr "" -#: client/src/login/loginModal/loginModal.partial.html:24 +#: client/src/login/loginModal/loginModal.partial.html:28 msgid "Maximum per-user sessions reached. Please sign in." msgstr "" @@ -1006,24 +1095,26 @@ msgstr "" #: client/src/dashboard/lists/jobs/jobs-list.partial.html:13 #: client/src/forms/Credentials.js:34 #: client/src/forms/Inventories.js:29 -#: client/src/forms/JobTemplates.js:34 +#: client/src/forms/JobTemplates.js:35 #: client/src/forms/Organizations.js:26 #: client/src/forms/Projects.js:31 #: client/src/forms/Teams.js:126 #: client/src/forms/Teams.js:27 +#: client/src/forms/Users.js:139 +#: client/src/forms/Users.js:164 #: client/src/forms/Users.js:190 -#: client/src/forms/Workflows.js:31 +#: client/src/forms/Workflows.js:33 #: client/src/inventory-scripts/inventory-scripts.form.js:25 #: client/src/inventory-scripts/inventory-scripts.list.js:20 #: client/src/lists/CompletedJobs.js:43 #: client/src/lists/Credentials.js:29 -#: client/src/lists/Inventories.js:47 +#: client/src/lists/Inventories.js:46 #: client/src/lists/PortalJobTemplates.js:24 #: client/src/lists/PortalJobs.js:32 #: client/src/lists/Projects.js:37 #: client/src/lists/ScheduledJobs.js:32 -#: client/src/lists/Teams.js:26 -#: client/src/lists/Templates.js:27 +#: client/src/lists/Teams.js:25 +#: client/src/lists/Templates.js:26 #: client/src/notifications/notificationTemplates.form.js:29 #: client/src/notifications/notificationTemplates.list.js:33 #: client/src/notifications/notifications.list.js:26 @@ -1034,12 +1125,12 @@ msgstr "" msgid "Network" msgstr "" -#: client/src/forms/JobTemplates.js:181 -#: client/src/forms/JobTemplates.js:192 +#: client/src/forms/JobTemplates.js:182 +#: client/src/forms/JobTemplates.js:193 msgid "Network Credential" msgstr "" -#: client/src/forms/JobTemplates.js:191 +#: client/src/forms/JobTemplates.js:192 msgid "Network credentials are used by Ansible networking modules to connect to and manage networking devices." msgstr "" @@ -1079,6 +1170,10 @@ msgstr "" msgid "New Workflow" msgstr "" +#: client/src/controllers/Users.js:174 +msgid "New user successfully created!" +msgstr "" + #: client/src/lists/ScheduledJobs.js:50 msgid "Next Run" msgstr "" @@ -1161,12 +1256,12 @@ msgstr "" msgid "OpenStack domains define administrative boundaries. It is only needed for Keystone v3 authentication URLs. Common scenarios include:" msgstr "" -#: client/src/forms/JobTemplates.js:346 -#: client/src/forms/Workflows.js:64 +#: client/src/forms/JobTemplates.js:347 +#: client/src/forms/Workflows.js:66 msgid "Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs in the Tower display." msgstr "" -#: client/src/forms/JobTemplates.js:283 +#: client/src/forms/JobTemplates.js:284 #: client/src/notifications/notificationTemplates.form.js:391 msgid "Options" msgstr "" @@ -1178,12 +1273,12 @@ msgstr "" #: client/src/forms/Projects.js:49 #: client/src/forms/Teams.js:39 #: client/src/forms/Users.js:59 -#: client/src/forms/Workflows.js:44 -#: client/src/forms/Workflows.js:50 +#: client/src/forms/Workflows.js:46 +#: client/src/forms/Workflows.js:52 #: client/src/inventory-scripts/inventory-scripts.form.js:37 #: client/src/inventory-scripts/inventory-scripts.list.js:30 -#: client/src/lists/Inventories.js:53 -#: client/src/lists/Teams.js:36 +#: client/src/lists/Inventories.js:52 +#: client/src/lists/Teams.js:35 #: client/src/notifications/notificationTemplates.form.js:41 msgid "Organization" msgstr "" @@ -1201,7 +1296,7 @@ msgstr "" msgid "Owners" msgstr "" -#: client/src/login/loginModal/loginModal.partial.html:64 +#: client/src/login/loginModal/loginModal.partial.html:68 msgid "PASSWORD" msgstr "" @@ -1223,8 +1318,8 @@ msgstr "" msgid "Pagerduty subdomain" msgstr "" -#: client/src/forms/JobTemplates.js:357 -#: client/src/forms/Workflows.js:75 +#: client/src/forms/JobTemplates.js:358 +#: client/src/forms/Workflows.js:77 msgid "Pass extra command line variables to the playbook. This is the %s or %s command line parameter for %s. Provide key/value pairs using either YAML or JSON." msgstr "" @@ -1278,17 +1373,22 @@ msgstr "" msgid "Period" msgstr "" +#: client/src/controllers/Projects.js:284 +#: client/src/controllers/Users.js:141 +msgid "Permission Error" +msgstr "" + #: client/src/forms/Credentials.js:432 #: client/src/forms/Inventories.js:142 -#: client/src/forms/JobTemplates.js:419 +#: client/src/forms/JobTemplates.js:403 #: client/src/forms/Organizations.js:64 #: client/src/forms/Projects.js:228 -#: client/src/forms/Workflows.js:113 +#: client/src/forms/Workflows.js:115 msgid "Permissions" msgstr "" -#: client/src/forms/JobTemplates.js:119 -#: client/src/forms/JobTemplates.js:130 +#: client/src/forms/JobTemplates.js:120 +#: client/src/forms/JobTemplates.js:131 msgid "Playbook" msgstr "" @@ -1304,37 +1404,18 @@ msgstr "" msgid "Please click the button below to visit Ansible's website to get a Tower license key." msgstr "" -#: client/src/shared/form-generator.js:844 -#: client/src/shared/form-generator.js:969 -msgid "Please enter a URL that begins with ssh, http or https. The URL may not contain the '@' character." -msgstr "" - -#: client/src/login/loginModal/loginModal.partial.html:74 +#: client/src/login/loginModal/loginModal.partial.html:78 msgid "Please enter a password." msgstr "" -#: client/src/login/loginModal/loginModal.partial.html:54 +#: client/src/login/loginModal/loginModal.partial.html:58 msgid "Please enter a username." msgstr "" -#: client/src/shared/form-generator.js:834 -#: client/src/shared/form-generator.js:959 -msgid "Please enter a valid email address." -msgstr "" - -#: client/src/shared/form-generator.js:829 -#: client/src/shared/form-generator.js:954 -msgid "Please enter a value." -msgstr "" - #: client/src/lists/CompletedJobs.js:13 msgid "Please save and run a job to view" msgstr "" -#: client/src/forms/Workflows.js:159 -msgid "Please save before adding a survey" -msgstr "" - #: client/src/notifications/notifications.list.js:15 msgid "Please save before adding notifications" msgstr "" @@ -1345,11 +1426,11 @@ msgstr "" #: client/src/forms/Inventories.js:138 #: client/src/forms/Inventories.js:91 -#: client/src/forms/JobTemplates.js:412 +#: client/src/forms/JobTemplates.js:396 #: client/src/forms/Organizations.js:57 #: client/src/forms/Projects.js:220 #: client/src/forms/Teams.js:110 -#: client/src/forms/Workflows.js:106 +#: client/src/forms/Workflows.js:108 msgid "Please save before assigning permissions" msgstr "" @@ -1362,7 +1443,7 @@ msgstr "" msgid "Please save before assigning to teams" msgstr "" -#: client/src/forms/Workflows.js:173 +#: client/src/forms/Workflows.js:184 msgid "Please save before defining the workflow graph" msgstr "" @@ -1370,6 +1451,14 @@ msgstr "" msgid "Please select a Credential." msgstr "" +#: client/src/forms/JobTemplates.js:150 +msgid "Please select a Machine Credential or check the Prompt on launch option." +msgstr "" + +#: client/src/forms/JobTemplates.js:83 +msgid "Please select an Inventory or check the Prompt on launch option." +msgstr "" + #: client/src/forms/WorkflowMaker.js:86 msgid "Please select an Inventory." msgstr "" @@ -1401,8 +1490,8 @@ msgstr "" msgid "Privilege Escalation Username" msgstr "" -#: client/src/forms/JobTemplates.js:113 -#: client/src/forms/JobTemplates.js:96 +#: client/src/forms/JobTemplates.js:114 +#: client/src/forms/JobTemplates.js:97 #: client/src/helpers/Credentials.js:103 msgid "Project" msgstr "" @@ -1438,30 +1527,30 @@ msgstr "" msgid "Projects" msgstr "" -#: client/src/forms/JobTemplates.js:158 -#: client/src/forms/JobTemplates.js:229 -#: client/src/forms/JobTemplates.js:260 -#: client/src/forms/JobTemplates.js:278 -#: client/src/forms/JobTemplates.js:368 -#: client/src/forms/JobTemplates.js:67 -#: client/src/forms/JobTemplates.js:91 +#: client/src/forms/JobTemplates.js:159 +#: client/src/forms/JobTemplates.js:230 +#: client/src/forms/JobTemplates.js:261 +#: client/src/forms/JobTemplates.js:279 +#: client/src/forms/JobTemplates.js:369 +#: client/src/forms/JobTemplates.js:68 +#: client/src/forms/JobTemplates.js:92 msgid "Prompt on launch" msgstr "" -#: client/src/forms/JobTemplates.js:252 -#: client/src/forms/JobTemplates.js:270 +#: client/src/forms/JobTemplates.js:253 +#: client/src/forms/JobTemplates.js:271 #: client/src/forms/WorkflowMaker.js:139 #: client/src/forms/WorkflowMaker.js:154 msgid "Provide a comma separated list of tags." msgstr "" -#: client/src/forms/JobTemplates.js:220 +#: client/src/forms/JobTemplates.js:221 #: client/src/forms/WorkflowMaker.js:123 msgid "Provide a host pattern to further constrain the list of hosts that will be managed or affected by the playbook. Multiple patterns can be separated by %s %s or %s" msgstr "" -#: client/src/forms/JobTemplates.js:312 -#: client/src/forms/JobTemplates.js:320 +#: client/src/forms/JobTemplates.js:313 +#: client/src/forms/JobTemplates.js:321 msgid "Provisioning Callback URL" msgstr "" @@ -1478,12 +1567,16 @@ msgstr "" msgid "RECENTLY USED JOB TEMPLATES" msgstr "" -#: client/src/lists/Projects.js:70 +#: client/src/lists/Projects.js:76 #: client/src/partials/jobs.html:15 #: client/src/portal-mode/portal-mode-jobs.partial.html:12 msgid "REFRESH" msgstr "" +#: client/src/forms/JobTemplates.js:99 +msgid "RESET" +msgstr "" + #: client/src/helpers/Credentials.js:98 msgid "RSA Private Key" msgstr "" @@ -1494,10 +1587,14 @@ msgid "Recipient List" msgstr "" #: client/src/bread-crumb/bread-crumb.partial.html:6 -#: client/src/lists/Projects.js:66 +#: client/src/lists/Projects.js:72 msgid "Refresh the page" msgstr "" +#: client/src/lists/CompletedJobs.js:75 +msgid "Relaunch using the same parameters" +msgstr "" + #: client/src/forms/Teams.js:144 #: client/src/forms/Users.js:214 msgid "Remove" @@ -1511,16 +1608,29 @@ msgstr "" msgid "Request License" msgstr "" +#: client/src/lists/Projects.js:42 +msgid "Revision" +msgstr "" + +#: client/src/controllers/Projects.js:657 +msgid "Revision #" +msgstr "" + #: client/src/forms/Credentials.js:454 #: client/src/forms/Inventories.js:120 #: client/src/forms/Inventories.js:166 #: client/src/forms/Organizations.js:88 +#: client/src/forms/Projects.js:250 #: client/src/forms/Teams.js:137 #: client/src/forms/Teams.js:99 #: client/src/forms/Users.js:201 msgid "Role" msgstr "" +#: client/src/controllers/Projects.js:657 +msgid "SCM Branch" +msgstr "" + #: client/src/forms/Projects.js:155 msgid "SCM Clean" msgstr "" @@ -1563,7 +1673,7 @@ msgstr "" msgid "SETTINGS" msgstr "" -#: client/src/login/loginModal/loginModal.partial.html:93 +#: client/src/login/loginModal/loginModal.partial.html:97 msgid "SIGN IN" msgstr "" @@ -1571,7 +1681,7 @@ msgstr "" msgid "SIGN IN WITH" msgstr "" -#: client/src/app.js:503 +#: client/src/app.js:509 msgid "SOCKETS" msgstr "" @@ -1600,15 +1710,11 @@ msgstr "" msgid "Satellite 6 Host" msgstr "" -#: client/src/shared/form-generator.js:1699 -msgid "Save" -msgstr "" - #: client/src/license/license.partial.html:122 msgid "Save successful!" msgstr "" -#: client/src/lists/Templates.js:93 +#: client/src/lists/Templates.js:91 msgid "Schedule" msgstr "" @@ -1620,7 +1726,7 @@ msgstr "" msgid "Schedule future SCM updates" msgstr "" -#: client/src/lists/Templates.js:96 +#: client/src/lists/Templates.js:94 msgid "Schedule future job template runs" msgstr "" @@ -1648,25 +1754,25 @@ msgstr "" msgid "Select from the list of directories found in the base path.Together the base path and the playbook directory provide the full path used to locate playbooks." msgstr "" -#: client/src/forms/JobTemplates.js:151 +#: client/src/forms/JobTemplates.js:152 #: client/src/forms/WorkflowMaker.js:67 msgid "Select the credential you want the job to use when accessing the remote hosts. Choose the credential containing the username and SSH key or password that Ansible will need to log into the remote hosts." msgstr "" -#: client/src/forms/JobTemplates.js:84 +#: client/src/forms/JobTemplates.js:85 #: client/src/forms/WorkflowMaker.js:88 msgid "Select the inventory containing the hosts you want this job to manage." msgstr "" -#: client/src/forms/JobTemplates.js:129 +#: client/src/forms/JobTemplates.js:130 msgid "Select the playbook to be executed by this job." msgstr "" -#: client/src/forms/JobTemplates.js:112 +#: client/src/forms/JobTemplates.js:113 msgid "Select the project containing the playbook you want this job to execute." msgstr "" -#: client/src/forms/JobTemplates.js:173 +#: client/src/forms/JobTemplates.js:174 msgid "Selecting an optional cloud credential in the job template will pass along the access credentials to the running playbook, allowing provisioning into the cloud without manually passing parameters to the included modules." msgstr "" @@ -1678,12 +1784,12 @@ msgstr "" msgid "Service Account Email Address" msgstr "" -#: client/src/forms/JobTemplates.js:59 +#: client/src/forms/JobTemplates.js:60 #: client/src/forms/WorkflowMaker.js:108 msgid "Setting the type to %s will execute the playbook and store any scanned facts for use with Tower's System Tracking feature." msgstr "" -#: client/src/forms/JobTemplates.js:56 +#: client/src/forms/JobTemplates.js:57 msgid "Setting the type to %s will not execute the playbook." msgstr "" @@ -1695,32 +1801,29 @@ msgstr "" msgid "Settings" msgstr "" -#: client/src/shared/form-generator.js:859 -msgid "Show" -msgstr "" - #: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:34 #: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:45 -#: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:66 +#: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:56 +#: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:77 msgid "Sign in with %s" msgstr "" -#: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:53 +#: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:64 msgid "Sign in with %s Organizations" msgstr "" -#: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:51 +#: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:62 msgid "Sign in with %s Teams" msgstr "" -#: client/src/forms/JobTemplates.js:265 -#: client/src/forms/JobTemplates.js:273 +#: client/src/forms/JobTemplates.js:266 +#: client/src/forms/JobTemplates.js:274 #: client/src/forms/WorkflowMaker.js:149 #: client/src/forms/WorkflowMaker.js:157 msgid "Skip Tags" msgstr "" -#: client/src/forms/JobTemplates.js:271 +#: client/src/forms/JobTemplates.js:272 #: client/src/forms/WorkflowMaker.js:155 msgid "Skip tags are useful when you have a large playbook, and you want to skip specific parts of a play or task." msgstr "" @@ -1750,7 +1853,7 @@ msgid "Split up your organization to associate content and control permissions f msgstr "" #: client/src/lists/PortalJobTemplates.js:42 -#: client/src/lists/Templates.js:88 +#: client/src/lists/Templates.js:86 msgid "Start a job using this template" msgstr "" @@ -1759,6 +1862,10 @@ msgstr "" msgid "Start an SCM update" msgstr "" +#: client/src/dashboard/hosts/dashboard-hosts.list.js:49 +msgid "Status" +msgstr "" + #: client/src/license/license.partial.html:121 msgid "Submit" msgstr "" @@ -1792,7 +1899,7 @@ msgstr "" msgid "System Auditor" msgstr "" -#: client/src/app.js:339 +#: client/src/app.js:341 msgid "TEAMS" msgstr "" @@ -1805,7 +1912,7 @@ msgstr "" msgid "TIME" msgstr "" -#: client/src/forms/JobTemplates.js:253 +#: client/src/forms/JobTemplates.js:254 #: client/src/forms/WorkflowMaker.js:140 msgid "Tags are useful when you have a large playbook, and you want to run a specific part of a play or task." msgstr "" @@ -1818,6 +1925,7 @@ msgstr "" #: client/src/forms/Inventories.js:126 #: client/src/forms/Inventories.js:173 #: client/src/forms/Organizations.js:95 +#: client/src/forms/Projects.js:256 msgid "Team Roles" msgstr "" @@ -1849,7 +1957,7 @@ msgstr "" msgid "The Project ID is the GCE assigned identification. It is constructed as two words followed by a three digit number. Such as:" msgstr "" -#: client/src/controllers/Projects.js:692 +#: client/src/controllers/Projects.js:693 msgid "The SCM update process is running." msgstr "" @@ -1865,7 +1973,7 @@ msgstr "" msgid "The host value" msgstr "" -#: client/src/forms/JobTemplates.js:207 +#: client/src/forms/JobTemplates.js:208 msgid "The number of parallel or simultaneous processes to use while executing the playbook. 0 signifies the default value from the %sansible configuration file%s." msgstr "" @@ -1893,13 +2001,16 @@ msgstr "" msgid "This is the tenant name. This value is usually the same as the username." msgstr "" +#: client/src/notifications/notifications.list.js:21 +msgid "This list is populated by notification templates added from the %sNotifications%s section" +msgstr "" + #: client/src/notifications/notificationTemplates.form.js:199 msgid "This must be of the form %s." msgstr "" -#: client/src/shared/form-generator.js:839 -#: client/src/shared/form-generator.js:964 -msgid "This value does not match the password you entered previously. Please confirm that password." +#: client/src/forms/Users.js:160 +msgid "This user is not a member of any teams" msgstr "" #: client/src/dashboard/lists/jobs/jobs-list.partial.html:14 @@ -1918,10 +2029,6 @@ msgstr "" msgid "To learn more about the IAM STS Token, refer to the %sAmazon documentation%s." msgstr "" -#: client/src/shared/form-generator.js:864 -msgid "Toggle the display of plaintext." -msgstr "" - #: client/src/notifications/shared/type-change.service.js:34 #: client/src/notifications/shared/type-change.service.js:40 msgid "Token" @@ -1934,9 +2041,9 @@ msgstr "" #: client/src/forms/WorkflowMaker.js:34 #: client/src/lists/CompletedJobs.js:50 #: client/src/lists/Credentials.js:39 -#: client/src/lists/Projects.js:42 +#: client/src/lists/Projects.js:48 #: client/src/lists/ScheduledJobs.js:42 -#: client/src/lists/Templates.js:32 +#: client/src/lists/Templates.js:31 #: client/src/notifications/notificationTemplates.form.js:54 #: client/src/notifications/notificationTemplates.list.js:38 #: client/src/notifications/notifications.list.js:31 @@ -1960,15 +2067,15 @@ msgid "Type an option on each line. The pound symbol (#) is not required." msgstr "" #: client/src/controllers/Projects.js:402 -#: client/src/controllers/Projects.js:683 +#: client/src/controllers/Projects.js:684 msgid "URL popover text" msgstr "" -#: client/src/login/loginModal/loginModal.partial.html:45 +#: client/src/login/loginModal/loginModal.partial.html:49 msgid "USERNAME" msgstr "" -#: client/src/app.js:363 +#: client/src/app.js:365 msgid "USERS" msgstr "" @@ -1976,6 +2083,10 @@ msgstr "" msgid "Update Not Found" msgstr "" +#: client/src/controllers/Projects.js:693 +msgid "Update in Progress" +msgstr "" + #: client/src/forms/Projects.js:173 msgid "Update on Launch" msgstr "" @@ -2005,6 +2116,7 @@ msgstr "" #: client/src/forms/Inventories.js:115 #: client/src/forms/Inventories.js:161 #: client/src/forms/Organizations.js:83 +#: client/src/forms/Projects.js:245 #: client/src/forms/Teams.js:94 msgid "User" msgstr "" @@ -2059,8 +2171,8 @@ msgstr "" msgid "Vault Password" msgstr "" -#: client/src/forms/JobTemplates.js:234 -#: client/src/forms/JobTemplates.js:241 +#: client/src/forms/JobTemplates.js:235 +#: client/src/forms/JobTemplates.js:242 msgid "Verbosity" msgstr "" @@ -2072,9 +2184,9 @@ msgstr "" #: client/src/dashboard/graphs/dashboard-graphs.partial.html:58 #: client/src/inventory-scripts/inventory-scripts.list.js:65 #: client/src/lists/Credentials.js:80 -#: client/src/lists/Inventories.js:86 -#: client/src/lists/Teams.js:70 -#: client/src/lists/Templates.js:117 +#: client/src/lists/Inventories.js:85 +#: client/src/lists/Teams.js:69 +#: client/src/lists/Templates.js:115 #: client/src/lists/Users.js:78 #: client/src/notifications/notificationTemplates.list.js:80 msgid "View" @@ -2088,7 +2200,8 @@ msgstr "" msgid "View JSON examples at %s" msgstr "" -#: client/src/shared/form-generator.js:1723 +#: client/src/forms/JobTemplates.js:450 +#: client/src/forms/Workflows.js:162 msgid "View Survey" msgstr "" @@ -2112,7 +2225,7 @@ msgstr "" msgid "View information about this version of Ansible Tower." msgstr "" -#: client/src/lists/Inventories.js:88 +#: client/src/lists/Inventories.js:87 msgid "View inventory" msgstr "" @@ -2124,15 +2237,15 @@ msgstr "" msgid "View notification" msgstr "" -#: client/src/lists/Teams.js:73 +#: client/src/lists/Teams.js:72 msgid "View team" msgstr "" -#: client/src/lists/Templates.js:119 +#: client/src/lists/Templates.js:117 msgid "View template" msgstr "" -#: client/src/lists/Projects.js:102 +#: client/src/lists/Projects.js:108 msgid "View the project" msgstr "" @@ -2144,7 +2257,7 @@ msgstr "" msgid "View user" msgstr "" -#: client/src/login/loginModal/loginModal.partial.html:13 +#: client/src/login/loginModal/loginModal.partial.html:17 msgid "Welcome to Ansible Tower!  Please sign in." msgstr "" @@ -2152,41 +2265,36 @@ msgstr "" msgid "Welcome to Ansible Tower! Please complete the steps below to acquire a license." msgstr "" -#: client/src/forms/JobTemplates.js:54 +#: client/src/forms/JobTemplates.js:55 #: client/src/forms/WorkflowMaker.js:104 msgid "When this template is submitted as a job, setting the type to %s will execute the playbook, running tasks on the selected hosts." msgstr "" -#: client/src/forms/Workflows.js:175 -#: client/src/shared/form-generator.js:1727 +#: client/src/forms/Workflows.js:186 msgid "Workflow Editor" msgstr "" -#: client/src/shared/form-generator.js:976 -msgid "Your password must be %d characters long." +#: client/src/lists/Templates.js:69 +msgid "Workflow Job Template" msgstr "" -#: client/src/shared/form-generator.js:981 -msgid "Your password must contain a lowercase letter." +#: client/src/controllers/Projects.js:468 +msgid "You do not have access to view this property" msgstr "" -#: client/src/shared/form-generator.js:991 -msgid "Your password must contain a number." +#: client/src/controllers/Projects.js:284 +msgid "You do not have permission to add a project." msgstr "" -#: client/src/shared/form-generator.js:986 -msgid "Your password must contain an uppercase letter." -msgstr "" - -#: client/src/shared/form-generator.js:996 -msgid "Your password must contain one of the following characters: %s" +#: client/src/controllers/Users.js:141 +msgid "You do not have permission to add a user." msgstr "" #: client/src/controllers/Projects.js:176 msgid "Your request to cancel the update was submitted to the task manager." msgstr "" -#: client/src/login/loginModal/loginModal.partial.html:18 +#: client/src/login/loginModal/loginModal.partial.html:22 msgid "Your session timed out due to inactivity. Please sign in." msgstr "" From ebb0f06b17e34119eb9fe28920f1a9c33b331a87 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 6 Dec 2016 12:12:08 -0500 Subject: [PATCH 021/595] Updating host field help text --- awx/main/models/inventory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index c16b89bcb2..e7183f356d 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -342,6 +342,7 @@ class Host(CommonModelNameNotUnique): max_length=1024, blank=True, default='', + help_text=_('The value used by the remote inventory source to uniquely identify the host'), ) variables = models.TextField( blank=True, From a9c1969aa058cc6a7e79f2acd379c6d0bcd36dbc Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 6 Dec 2016 09:13:19 -0800 Subject: [PATCH 022/595] adding useful comment to $state.defaultErrorHandler --- awx/ui/client/src/app.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index ec6639ce7b..9009cc0779 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -386,8 +386,9 @@ var tower = angular.module('Tower', [ $rootScope.$stateParams = $stateParams; $state.defaultErrorHandler(function() { - // Do not log transitionTo errors - // $log.debug("transitionTo error: " + error ); + // Do not log transitionTo errors. This function, + // left empty, will prevent errors being displayed on the + // JS console that are caused by ui-router transitions. }); I18NInit(); From 51461a27dafabf8853df062fc4ca3d1edde33fda Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 6 Dec 2016 12:18:17 -0500 Subject: [PATCH 023/595] Adding help text to various job fields --- awx/main/models/unified_jobs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 63c3e3196a..484831eacf 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -431,6 +431,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique blank=True, default='', editable=False, + help_text=_("The Tower node the job executed on."), ) notifications = models.ManyToManyField( 'Notification', @@ -456,16 +457,19 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique null=True, default=None, editable=False, + help_text=_("The date and time the job was queued for starting."), ) finished = models.DateTimeField( null=True, default=None, editable=False, + help_text=_("The date and time the job finished execution."), ) elapsed = models.DecimalField( max_digits=12, decimal_places=3, editable=False, + help_text=_("Elapsed time in seconds that the job ran."), ) job_args = models.TextField( blank=True, @@ -487,6 +491,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique blank=True, default='', editable=False, + help_text=_("A status field to indicate the state of the job if it wasn't able to run and capture stdout"), ) start_args = models.TextField( blank=True, From d62bad536b6f2ae3d6b51a869426fe1adc88629b Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 6 Dec 2016 12:22:08 -0500 Subject: [PATCH 024/595] add mkdir command to start_development script to cause collectstatic not to error --- tools/docker-compose/start_development.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/docker-compose/start_development.sh b/tools/docker-compose/start_development.sh index 688eeded33..b6fe94e854 100755 --- a/tools/docker-compose/start_development.sh +++ b/tools/docker-compose/start_development.sh @@ -41,6 +41,7 @@ make migrate make init mkdir -p /tower_devel/awx/public/static +mkdir -p /tower_devel/awx/ui/static # Start the service make honcho From 7bd8b8d051fe57e35c8f1ce62dcf5a31bf76de1b Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Tue, 6 Dec 2016 12:26:26 -0500 Subject: [PATCH 025/595] Fix syntax, regenerate pot file. Thanks jshint! --- awx/ui/client/src/controllers/Users.js | 2 +- awx/ui/client/src/helpers/Credentials.js | 4 +- awx/ui/client/src/shared/form-generator.js | 6 +- awx/ui/po/ansible-tower-ui.pot | 131 +++++++++++++++++++++ 4 files changed, 137 insertions(+), 6 deletions(-) diff --git a/awx/ui/client/src/controllers/Users.js b/awx/ui/client/src/controllers/Users.js index e53f214e0f..cf7f63b9a1 100644 --- a/awx/ui/client/src/controllers/Users.js +++ b/awx/ui/client/src/controllers/Users.js @@ -114,7 +114,7 @@ UsersList.$inject = ['$scope', '$rootScope', '$stateParams', export function UsersAdd($scope, $rootScope, $stateParams, UserForm, GenerateForm, Rest, Alert, ProcessErrors, ReturnToCaller, ClearScope, - GetBasePath, ResetForm, Wait, CreateSelect2, $state, $location) { + GetBasePath, ResetForm, Wait, CreateSelect2, $state, $location, i18n) { ClearScope(); diff --git a/awx/ui/client/src/helpers/Credentials.js b/awx/ui/client/src/helpers/Credentials.js index fb7477d61f..a58c37bce8 100644 --- a/awx/ui/client/src/helpers/Credentials.js +++ b/awx/ui/client/src/helpers/Credentials.js @@ -223,8 +223,8 @@ angular.module('CredentialsHelper', ['Utilities']) } ]) -.factory('FormSave', ['$rootScope', '$location', 'Alert', 'Rest', 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm', 'ReturnToCaller', 'Wait', '$state', - function ($rootScope, $location, Alert, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller, Wait, $state) { +.factory('FormSave', ['$rootScope', '$location', 'Alert', 'Rest', 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm', 'ReturnToCaller', 'Wait', '$state', 'i18n', + function ($rootScope, $location, Alert, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller, Wait, $state, i18n) { return function (params) { var scope = params.scope, mode = params.mode, diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 3c931b64d2..7771db2922 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1186,9 +1186,9 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "
"; if (field.max !== undefined) { - html += i18n.sprintf(i18n._("Please enter a number greater than %d and less than %d."), field.min, field.max) + html += i18n.sprintf(i18n._("Please enter a number greater than %d and less than %d."), field.min, field.max); } else { - html += i18n.sprintf(i18n._("Please enter a number greater than %d.", field.min) + html += i18n.sprintf(i18n._("Please enter a number greater than %d."), field.min); } html += "
\n"; } @@ -1295,7 +1295,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat if (field.required || field.awRequiredWhen) { html += "
" + i18n._("Please select a value.)" + "
\n"; + this.form.name + '_form.' + fld + ".$error.required\">" + i18n._("Please select a value.") + "\n"; } html += "
\n"; diff --git a/awx/ui/po/ansible-tower-ui.pot b/awx/ui/po/ansible-tower-ui.pot index 62433d3bc7..e3226dc3f4 100644 --- a/awx/ui/po/ansible-tower-ui.pot +++ b/awx/ui/po/ansible-tower-ui.pot @@ -120,6 +120,7 @@ msgstr "" #: client/src/forms/JobTemplates.js:459 #: client/src/forms/Workflows.js:171 +#: client/src/shared/form-generator.js:1703 msgid "Add Survey" msgstr "" @@ -147,6 +148,10 @@ msgstr "" msgid "Add user to team" msgstr "" +#: client/src/shared/form-generator.js:1450 +msgid "Admin" +msgstr "" + #: client/src/dashboard/graphs/dashboard-graphs.partial.html:37 #: client/src/dashboard/graphs/dashboard-graphs.partial.html:43 #: client/src/dashboard/graphs/dashboard-graphs.partial.html:65 @@ -194,6 +199,10 @@ msgstr "" msgid "Ask at runtime?" msgstr "" +#: client/src/shared/form-generator.js:1452 +msgid "Auditor" +msgstr "" + #: client/src/forms/Credentials.js:73 msgid "Authentication for network device access. This can include SSH keys, usernames, passwords, and authorize information. Network credentials are used when submitting jobs to run playbooks against network devices." msgstr "" @@ -256,6 +265,10 @@ msgstr "" msgid "Call to get project failed. GET status:" msgstr "" +#: client/src/shared/form-generator.js:1691 +msgid "Cancel" +msgstr "" + #: client/src/controllers/Projects.js:196 msgid "Cancel Not Allowed" msgstr "" @@ -268,6 +281,10 @@ msgstr "" msgid "Canceled. Click for details" msgstr "" +#: client/src/shared/form-generator.js:1084 +msgid "Choose a %s" +msgstr "" + #: client/src/license/license.partial.html:97 msgid "Choose your license file, agree to the End User License Agreement, and click submit." msgstr "" @@ -300,6 +317,10 @@ msgstr "" msgid "Client Secret" msgstr "" +#: client/src/shared/form-generator.js:1695 +msgid "Close" +msgstr "" + #: client/src/forms/JobTemplates.js:164 #: client/src/forms/JobTemplates.js:176 msgid "Cloud Credential" @@ -525,6 +546,7 @@ msgid "Destination SMS Number" msgstr "" #: client/src/license/license.partial.html:5 +#: client/src/shared/form-generator.js:1481 msgid "Details" msgstr "" @@ -563,6 +585,7 @@ msgstr "" #: client/src/forms/JobTemplates.js:466 #: client/src/forms/Workflows.js:178 +#: client/src/shared/form-generator.js:1707 msgid "Edit Survey" msgstr "" @@ -990,6 +1013,7 @@ msgstr "" #: client/src/lists/PortalJobTemplates.js:39 #: client/src/lists/Templates.js:83 +#: client/src/shared/form-generator.js:1699 msgid "Launch" msgstr "" @@ -1037,6 +1061,10 @@ msgstr "" msgid "Live events: error connecting to the Tower server." msgstr "" +#: client/src/shared/form-generator.js:1962 +msgid "Loading..." +msgstr "" + #: client/src/main-menu/main-menu.partial.html:188 msgid "Log Out" msgstr "" @@ -1240,6 +1268,14 @@ msgstr "" msgid "Number associated with the \"Messaging Service\" in Twilio." msgstr "" +#: client/src/shared/form-generator.js:547 +msgid "OFF" +msgstr "" + +#: client/src/shared/form-generator.js:545 +msgid "ON" +msgstr "" + #: client/src/organizations/list/organizations-list.partial.html:6 msgid "ORGANIZATIONS" msgstr "" @@ -1301,6 +1337,7 @@ msgid "PASSWORD" msgstr "" #: client/src/organizations/list/organizations-list.partial.html:44 +#: client/src/shared/form-generator.js:1865 #: client/src/shared/list-generator/list-generator.factory.js:245 msgid "PLEASE ADD ITEMS TO THIS LIST" msgstr "" @@ -1404,6 +1441,23 @@ msgstr "" msgid "Please click the button below to visit Ansible's website to get a Tower license key." msgstr "" +#: client/src/shared/form-generator.js:828 +#: client/src/shared/form-generator.js:953 +msgid "Please enter a URL that begins with ssh, http or https. The URL may not contain the '@' character." +msgstr "" + +#: client/src/shared/form-generator.js:1189 +msgid "Please enter a number greater than %d and less than %d." +msgstr "" + +#: client/src/shared/form-generator.js:1191 +msgid "Please enter a number greater than %d." +msgstr "" + +#: client/src/shared/form-generator.js:1183 +msgid "Please enter a number." +msgstr "" + #: client/src/login/loginModal/loginModal.partial.html:78 msgid "Please enter a password." msgstr "" @@ -1412,6 +1466,17 @@ msgstr "" msgid "Please enter a username." msgstr "" +#: client/src/shared/form-generator.js:818 +#: client/src/shared/form-generator.js:943 +msgid "Please enter a valid email address." +msgstr "" + +#: client/src/shared/form-generator.js:1044 +#: client/src/shared/form-generator.js:813 +#: client/src/shared/form-generator.js:938 +msgid "Please enter a value." +msgstr "" + #: client/src/lists/CompletedJobs.js:13 msgid "Please save and run a job to view" msgstr "" @@ -1455,6 +1520,21 @@ msgstr "" msgid "Please select a Machine Credential or check the Prompt on launch option." msgstr "" +#: client/src/shared/form-generator.js:1224 +msgid "Please select a number between" +msgstr "" + +#: client/src/shared/form-generator.js:1220 +msgid "Please select a number." +msgstr "" + +#: client/src/shared/form-generator.js:1111 +#: client/src/shared/form-generator.js:1180 +#: client/src/shared/form-generator.js:1298 +#: client/src/shared/form-generator.js:1403 +msgid "Please select a value." +msgstr "" + #: client/src/forms/JobTemplates.js:83 msgid "Please select an Inventory or check the Prompt on launch option." msgstr "" @@ -1463,6 +1543,10 @@ msgstr "" msgid "Please select an Inventory." msgstr "" +#: client/src/shared/form-generator.js:1217 +msgid "Please select at least one value." +msgstr "" + #: client/src/notifications/shared/type-change.service.js:27 msgid "Port" msgstr "" @@ -1710,6 +1794,10 @@ msgstr "" msgid "Satellite 6 Host" msgstr "" +#: client/src/shared/form-generator.js:1687 +msgid "Save" +msgstr "" + #: client/src/license/license.partial.html:122 msgid "Save successful!" msgstr "" @@ -1801,6 +1889,10 @@ msgstr "" msgid "Settings" msgstr "" +#: client/src/shared/form-generator.js:843 +msgid "Show" +msgstr "" + #: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:34 #: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:45 #: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:56 @@ -1953,6 +2045,10 @@ msgstr "" msgid "Test notification" msgstr "" +#: client/src/shared/form-generator.js:1409 +msgid "That value was not found. Please enter or select a valid value." +msgstr "" + #: client/src/helpers/Credentials.js:105 msgid "The Project ID is the GCE assigned identification. It is constructed as two words followed by a three digit number. Such as:" msgstr "" @@ -2013,6 +2109,11 @@ msgstr "" msgid "This user is not a member of any teams" msgstr "" +#: client/src/shared/form-generator.js:823 +#: client/src/shared/form-generator.js:948 +msgid "This value does not match the password you entered previously. Please confirm that password." +msgstr "" + #: client/src/dashboard/lists/jobs/jobs-list.partial.html:14 msgid "Time" msgstr "" @@ -2029,6 +2130,10 @@ msgstr "" msgid "To learn more about the IAM STS Token, refer to the %sAmazon documentation%s." msgstr "" +#: client/src/shared/form-generator.js:848 +msgid "Toggle the display of plaintext." +msgstr "" + #: client/src/notifications/shared/type-change.service.js:34 #: client/src/notifications/shared/type-change.service.js:40 msgid "Token" @@ -2202,6 +2307,7 @@ msgstr "" #: client/src/forms/JobTemplates.js:450 #: client/src/forms/Workflows.js:162 +#: client/src/shared/form-generator.js:1711 msgid "View Survey" msgstr "" @@ -2271,6 +2377,7 @@ msgid "When this template is submitted as a job, setting the type to %s will exe msgstr "" #: client/src/forms/Workflows.js:186 +#: client/src/shared/form-generator.js:1715 msgid "Workflow Editor" msgstr "" @@ -2290,6 +2397,26 @@ msgstr "" msgid "You do not have permission to add a user." msgstr "" +#: client/src/shared/form-generator.js:960 +msgid "Your password must be %d characters long." +msgstr "" + +#: client/src/shared/form-generator.js:965 +msgid "Your password must contain a lowercase letter." +msgstr "" + +#: client/src/shared/form-generator.js:975 +msgid "Your password must contain a number." +msgstr "" + +#: client/src/shared/form-generator.js:970 +msgid "Your password must contain an uppercase letter." +msgstr "" + +#: client/src/shared/form-generator.js:980 +msgid "Your password must contain one of the following characters: %s" +msgstr "" + #: client/src/controllers/Projects.js:176 msgid "Your request to cancel the update was submitted to the task manager." msgstr "" @@ -2298,6 +2425,10 @@ msgstr "" msgid "Your session timed out due to inactivity. Please sign in." msgstr "" +#: client/src/shared/form-generator.js:1224 +msgid "and" +msgstr "" + #: client/src/forms/Credentials.js:139 #: client/src/forms/Credentials.js:362 msgid "set in helpers/credentials" From 8dbaea59b4e8489667258a9f381d638d68ef1838 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 6 Dec 2016 09:29:33 -0800 Subject: [PATCH 026/595] Using $log for debugging transitionTo errors --- awx/ui/client/src/app.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 9009cc0779..3d23bdcf8f 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -385,10 +385,8 @@ var tower = angular.module('Tower', [ }; $rootScope.$stateParams = $stateParams; - $state.defaultErrorHandler(function() { - // Do not log transitionTo errors. This function, - // left empty, will prevent errors being displayed on the - // JS console that are caused by ui-router transitions. + $state.defaultErrorHandler(function(error) { + $log.debug(`$state.defaultErrorHandler: ${error}`); }); I18NInit(); From 6efeeeb0839ed908c895038883840e8a8b2b4de5 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 6 Dec 2016 12:29:56 -0500 Subject: [PATCH 027/595] Adding missing period for timeout --- awx/main/models/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 2d045cee6f..6c76143f40 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -113,7 +113,7 @@ class ProjectOptions(models.Model): timeout = models.IntegerField( blank=True, default=0, - help_text=_("The amount of time to run before the task is canceled"), + help_text=_("The amount of time to run before the task is canceled."), ) def clean_scm_type(self): From 4295ab3e4a78357b7c11e9c1a194bff79425aec6 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 6 Dec 2016 12:55:51 -0500 Subject: [PATCH 028/595] Show SAML errors that aren't tied to a specific IdP. --- awx/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/views.py b/awx/api/views.py index e1009e6bfb..734d1159b6 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -534,7 +534,7 @@ class AuthView(APIView): saml_backend_data = dict(backend_data.items()) saml_backend_data['login_url'] = '%s?idp=%s' % (login_url, idp) full_backend_name = '%s:%s' % (name, idp) - if err_backend == full_backend_name and err_message: + if (err_backend == full_backend_name or err_backend == name) and err_message: saml_backend_data['error'] = err_message data[full_backend_name] = saml_backend_data else: From 417d7c29ee6e7941e5b8c259a167a149964e3414 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 6 Dec 2016 12:56:25 -0500 Subject: [PATCH 029/595] Allow SAML entity ID to be any string, not required to be a URL. --- awx/sso/conf.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/awx/sso/conf.py b/awx/sso/conf.py index 4bde08e55a..c3ab7f7e56 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -924,13 +924,12 @@ register( register( 'SOCIAL_AUTH_SAML_SP_ENTITY_ID', - field_class=fields.URLField, - schemes=('http', 'https'), + field_class=fields.CharField, allow_blank=True, default='', label=_('SAML Service Provider Entity ID'), - help_text=_('Set to a URL for a domain name you own (does not need to be a ' - 'valid URL; only used as a unique ID).'), + help_text=_('The application-defined unique identifier used as the ' + 'audience of the SAML service provider (SP) configuration.'), category=_('SAML'), category_slug='saml', feature_required='enterprise_auth', From 1e157c2255eb9f0eda70f580f9ddd5efb6b00104 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 6 Dec 2016 12:57:01 -0500 Subject: [PATCH 030/595] Display error instead of raising 500 for invalid SAML config to generate metadata. --- awx/sso/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/sso/views.py b/awx/sso/views.py index a25aabf511..5d34234464 100644 --- a/awx/sso/views.py +++ b/awx/sso/views.py @@ -83,7 +83,11 @@ class MetadataView(View): 'saml', redirect_uri=complete_url, ) - metadata, errors = saml_backend.generate_metadata_xml() + try: + metadata, errors = saml_backend.generate_metadata_xml() + except Exception as e: + logger.exception('unable to generate SAML metadata') + errors = e if not errors: return HttpResponse(content=metadata, content_type='text/xml') else: From 71c600cad0be32c829fb5c758f1642bbd1bcfbce Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 6 Dec 2016 13:18:34 -0500 Subject: [PATCH 031/595] more missing punctuation --- awx/main/models/schedules.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/main/models/schedules.py b/awx/main/models/schedules.py index 243201d377..21ecf49916 100644 --- a/awx/main/models/schedules.py +++ b/awx/main/models/schedules.py @@ -66,29 +66,29 @@ class Schedule(CommonModel): ) enabled = models.BooleanField( default=True, - help_text=_("Enables processing of this schedule by Tower") + help_text=_("Enables processing of this schedule by Tower.") ) dtstart = models.DateTimeField( null=True, default=None, editable=False, - help_text=_("The first occurrence of the schedule occurs on or after this time") + help_text=_("The first occurrence of the schedule occurs on or after this time.") ) dtend = models.DateTimeField( null=True, default=None, editable=False, - help_text=_("The last occurrence of the schedule occurs before this time, aftewards the schedule expires") + help_text=_("The last occurrence of the schedule occurs before this time, aftewards the schedule expires.") ) rrule = models.CharField( max_length=255, - help_text=_("A value representing the schedules iCal recurrence rule") + help_text=_("A value representing the schedules iCal recurrence rule.") ) next_run = models.DateTimeField( null=True, default=None, editable=False, - help_text=_("The next time that the scheduled action will run") + help_text=_("The next time that the scheduled action will run.") ) extra_data = JSONField( blank=True, From 49a5c136dd28de5474a4f29d3902358abf39a9b6 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 6 Dec 2016 13:29:53 -0500 Subject: [PATCH 032/595] add vmware deps to ansible venv --- requirements/requirements_ansible.in | 1 + requirements/requirements_ansible.txt | 44 +++++++++++++++------------ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/requirements/requirements_ansible.in b/requirements/requirements_ansible.in index 98981f2571..8027e6a92f 100644 --- a/requirements/requirements_ansible.in +++ b/requirements/requirements_ansible.in @@ -3,6 +3,7 @@ apache-libcloud==1.3.0 azure==2.0.0rc6 kombu==3.0.35 boto==2.43.0 +psphere==0.5.2 psutil==5.0.0 secretstorage==2.3.1 shade==1.13.1 diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index acba0d65ae..c3019c2dff 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -4,13 +4,14 @@ # # pip-compile --output-file requirements_ansible.txt requirements_ansible.in # --e git+https://github.com/chrismeyersfsu/pyrax@tower#egg=pyrax +git+https://github.com/chrismeyersfsu/pyrax@tower#egg=pyrax +adal==0.4.3 # via msrestazure amqp==1.4.9 # via kombu anyjson==0.3.3 # via kombu apache-libcloud==1.3.0 appdirs==1.4.0 # via os-client-config, python-ironicclient azure-batch==1.0.0 # via azure -azure-common==1.1.4 # via azure-batch, azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-keyvault, azure-mgmt-logic, azure-mgmt-network, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-storage, azure-servicebus, azure-servicemanagement-legacy, azure-storage +azure-common[autorest]==1.1.4 # via azure-batch, azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-keyvault, azure-mgmt-logic, azure-mgmt-network, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-storage, azure-servicebus, azure-servicemanagement-legacy, azure-storage azure-mgmt-batch==1.0.0 # via azure-mgmt azure-mgmt-compute==0.30.0rc6 # via azure-mgmt azure-mgmt-keyvault==0.30.0rc6 # via azure-mgmt @@ -27,15 +28,15 @@ azure-servicebus==0.20.3 # via azure azure-servicemanagement-legacy==0.20.4 # via azure azure-storage==0.33.0 # via azure azure==2.0.0rc6 -babel==2.3.4 # via osc-lib, oslo.i18n, python-cinderclient, python-glanceclient, python-heatclient, python-magnumclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient +Babel==2.3.4 # via osc-lib, oslo.i18n, python-cinderclient, python-glanceclient, python-heatclient, python-magnumclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient boto==2.43.0 certifi==2016.9.26 # via msrest cffi==1.9.1 # via cryptography chardet==2.3.0 # via msrest cliff==2.3.0 # via osc-lib, python-designateclient, python-heatclient, python-mistralclient, python-neutronclient, python-openstackclient cmd2==0.6.9 # via cliff -cryptography==1.6 # via azure-storage, python-magnumclient, secretstorage -debtcollector==1.9.0 # via oslo.config, oslo.utils, python-designateclient, python-keystoneclient, python-neutronclient +cryptography==1.6 # via adal, azure-storage, python-magnumclient, secretstorage +debtcollector==1.10.0 # via oslo.config, oslo.utils, python-designateclient, python-keystoneclient, python-neutronclient decorator==4.0.10 # via python-magnumclient, shade dogpile.cache==0.6.2 # via python-ironicclient, shade enum34==1.1.6 # via cryptography, msrest @@ -51,14 +52,14 @@ jmespath==0.9.0 # via shade jsonpatch==1.14 # via shade, warlock jsonpointer==1.10 # via jsonpatch jsonschema==2.5.1 # via python-designateclient, python-ironicclient, warlock -keyring==10.0.2 # via msrest -keystoneauth1==2.15.0 # via openstacksdk, os-client-config, osc-lib, python-cinderclient, python-designateclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient, shade +keyring==10.1 # via msrest, msrestazure +keystoneauth1==2.16.0 # via openstacksdk, os-client-config, osc-lib, python-cinderclient, python-designateclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient, shade kombu==3.0.35 mock==2.0.0 monotonic==1.2 # via oslo.utils msgpack-python==0.4.8 # via oslo.serialization msrest==0.4.4 # via azure-common, msrestazure -msrestazure==0.4.4 # via azure-common +msrestazure==0.4.5 # via azure-common munch==2.0.4 # via shade netaddr==0.7.18 # via oslo.config, oslo.utils, python-neutronclient netifaces==0.10.5 # via oslo.utils, shade @@ -69,46 +70,49 @@ os-diskconfig-python-novaclient-ext==0.1.3 # via rackspace-novaclient os-networksv2-python-novaclient-ext==0.26 # via rackspace-novaclient os-virtual-interfacesv2-python-novaclient-ext==0.20 # via rackspace-novaclient osc-lib==1.2.0 # via python-designateclient, python-heatclient, python-ironicclient, python-mistralclient, python-neutronclient, python-openstackclient -oslo.config==3.19.0 # via python-keystoneclient -oslo.i18n==3.10.0 # via osc-lib, oslo.config, oslo.utils, python-cinderclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient -oslo.serialization==2.14.0 # via python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-neutronclient, python-novaclient -oslo.utils==3.18.0 # via osc-lib, oslo.serialization, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient +oslo.config==3.20.0 # via python-keystoneclient +oslo.i18n==3.11.0 # via osc-lib, oslo.config, oslo.utils, python-cinderclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient +oslo.serialization==2.15.0 # via python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-neutronclient, python-novaclient +oslo.utils==3.19.0 # via osc-lib, oslo.serialization, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient pbr==1.10.0 # via cliff, debtcollector, keystoneauth1, mock, openstacksdk, osc-lib, oslo.i18n, oslo.serialization, oslo.utils, positional, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient, requestsexceptions, shade, stevedore positional==1.1.1 # via keystoneauth1, python-keystoneclient -prettytable==0.7.2 # via cliff, python-cinderclient, python-glanceclient, python-heatclient, python-ironicclient, python-magnumclient, python-novaclient, python-troveclient +PrettyTable==0.7.2 # via cliff, python-cinderclient, python-glanceclient, python-heatclient, python-ironicclient, python-magnumclient, python-novaclient, python-troveclient +psphere==0.5.2 psutil==5.0.0 pyasn1==0.1.9 # via cryptography pycparser==2.17 # via cffi +PyJWT==1.4.2 # via adal pyparsing==2.1.10 # via cliff, cmd2, oslo.utils python-cinderclient==1.9.0 # via python-openstackclient, shade -python-dateutil==2.6.0 # via azure-storage +python-dateutil==2.6.0 # via adal, azure-storage python-designateclient==2.3.0 # via shade python-glanceclient==2.5.0 # via python-openstackclient, shade python-heatclient==1.6.1 # via shade python-ironicclient==1.8.0 # via shade -python-keystoneclient==3.7.0 # via python-glanceclient, python-mistralclient, python-openstackclient, shade +python-keystoneclient==3.8.0 # via python-glanceclient, python-mistralclient, python-openstackclient, shade python-magnumclient==2.3.1 # via shade -python-mistralclient==2.1.1 # via python-troveclient +python-mistralclient==2.1.2 # via python-troveclient python-neutronclient==6.0.0 # via shade python-novaclient==6.0.0 # via ip-associations-python-novaclient-ext, os-diskconfig-python-novaclient-ext, os-networksv2-python-novaclient-ext, os-virtual-interfacesv2-python-novaclient-ext, python-openstackclient, rackspace-auth-openstack, rackspace-novaclient, rax-default-network-flags-python-novaclient-ext, rax-scheduled-images-python-novaclient-ext, shade python-openstackclient==3.4.1 # via python-ironicclient python-swiftclient==3.2.0 # via python-heatclient, python-troveclient, shade python-troveclient==2.6.0 # via shade -pytz==2016.7 # via babel, oslo.serialization, oslo.utils -pyyaml==3.12 # via cliff, os-client-config, python-heatclient, python-ironicclient, python-mistralclient +pytz==2016.10 # via babel, oslo.serialization, oslo.utils +PyYAML==3.12 # via cliff, os-client-config, psphere, python-heatclient, python-ironicclient, python-mistralclient rackspace-auth-openstack==1.3 # via rackspace-novaclient rackspace-novaclient==2.1 rax-default-network-flags-python-novaclient-ext==0.4.0 # via rackspace-novaclient rax-scheduled-images-python-novaclient-ext==0.3.1 # via rackspace-novaclient requests-oauthlib==0.7.0 # via msrest -requests==2.12.1 # via azure-servicebus, azure-servicemanagement-legacy, azure-storage, keystoneauth1, msrest, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-swiftclient, python-troveclient, requests-oauthlib +requests==2.11.1 # via adal, azure-servicebus, azure-servicemanagement-legacy, azure-storage, keystoneauth1, msrest, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-swiftclient, python-troveclient, requests-oauthlib requestsexceptions==1.1.3 # via os-client-config, shade rfc3986==0.4.1 # via oslo.config secretstorage==2.3.1 shade==1.13.1 simplejson==3.10.0 # via osc-lib, python-cinderclient, python-neutronclient, python-novaclient, python-troveclient six==1.10.0 # via cliff, cryptography, debtcollector, keystoneauth1, mock, openstacksdk, osc-lib, oslo.config, oslo.i18n, oslo.serialization, oslo.utils, python-cinderclient, python-dateutil, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-openstackclient, python-swiftclient, python-troveclient, shade, stevedore, warlock -stevedore==1.18.0 # via cliff, keystoneauth1, openstacksdk, osc-lib, oslo.config, python-designateclient, python-keystoneclient, python-magnumclient +stevedore==1.19.0 # via cliff, keystoneauth1, openstacksdk, osc-lib, oslo.config, python-designateclient, python-keystoneclient, python-magnumclient +suds==0.4 # via psphere unicodecsv==0.14.1 # via cliff warlock==1.2.0 # via python-glanceclient wrapt==1.10.8 # via debtcollector, positional From cd11a9c1c2405c598286f78da67ec9239565eb17 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 6 Dec 2016 13:32:33 -0500 Subject: [PATCH 033/595] peg compose tag to active branch --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 63b5ac259e..2e29eb174d 100644 --- a/Makefile +++ b/Makefile @@ -10,9 +10,8 @@ DEPS_SCRIPT ?= packaging/bundle/deps.py GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) GCLOUD_AUTH ?= $(shell gcloud auth print-access-token) -COMPOSE_TAG ?= devel # NOTE: This defaults the container image version to the branch that's active -# COMPOSE_TAG ?= $(GIT_BRANCH) +COMPOSE_TAG ?= $(GIT_BRANCH) COMPOSE_HOST ?= $(shell hostname) From 24b858c6fb683b7a337cca10e711cba756a91e5c Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 6 Dec 2016 13:33:14 -0500 Subject: [PATCH 034/595] Switch away from deepcopy to improve performance --- awx/lib/tower_display_callback/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/lib/tower_display_callback/module.py b/awx/lib/tower_display_callback/module.py index 457455e513..e61ef17624 100644 --- a/awx/lib/tower_display_callback/module.py +++ b/awx/lib/tower_display_callback/module.py @@ -110,7 +110,7 @@ class BaseCallbackModule(CallbackBase): event_data.setdefault('uuid', str(uuid.uuid4())) if 'res' in event_data: - event_data['res'] = self.censor_result(copy.deepcopy(event_data['res'])) + event_data['res'] = self.censor_result(copy.copy(event_data['res'])) res = event_data.get('res', None) if res and isinstance(res, dict): if 'artifact_data' in res: From 93fd3de34f039e65eff32b4403bf57702f3266b0 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 2 Dec 2016 12:15:19 -0500 Subject: [PATCH 035/595] move anchor tag stdout line dom to parent element --- awx/ui/client/src/job-results/parse-stdout.service.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js index 7469333aae..8d6564f3f0 100644 --- a/awx/ui/client/src/job-results/parse-stdout.service.js +++ b/awx/ui/client/src/job-results/parse-stdout.service.js @@ -64,12 +64,12 @@ export default ['$log', 'moment', function($log, moment){ return line; }, // adds anchor tags and tooltips to host status lines - getAnchorTags: function(event, line){ + getAnchorTags: function(event){ if(event.event_name.indexOf("runner_") === -1){ - return line; + return `"`; } else{ - return `${line}`; + return ` JobResultsStdOut-stdoutColumn--clickable" ui-sref="jobDetail.host-event.stdout({eventId: ${event.id}, taskId: ${event.parent} })" aw-tool-tip="Event ID: ${event.id}
Status: ${event.event_display}
Click for details" data-placement="top"`; } }, @@ -104,7 +104,8 @@ export default ['$log', 'moment', function($log, moment){ if (event.event_data.play_uuid) { string += " play_" + event.event_data.play_uuid; } - } else { + } else if (event.event_name !== "playbook_on_stats"){ + string += " not_skeleton"; // host status or debug line // these get classed by their parent play if applicable @@ -216,7 +217,7 @@ export default ['$log', 'moment', function($log, moment){ return `
${this.getCollapseIcon(event, lineArr[1])}${lineArr[0]}
-
${this.getAnchorTags(event, this.prettify(lineArr[1]))} ${this.getStartTimeBadge(event, lineArr[1] )}
+
diff --git a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less index cc34d11c2f..408e4cbb32 100644 --- a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less +++ b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less @@ -3,13 +3,16 @@ @breakpoint-md: 1200px; .JobResultsStdOut { - height: ~"calc(100% - 70px)"; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; } .JobResultsStdOut-toolbar { + flex: initial; display: flex; - height: 38px; - margin-top: 15px; border: 1px solid @default-list-header-bg; border-bottom: 0px; border-radius: 5px; @@ -28,7 +31,7 @@ display: flex; justify-content: space-between; width: 70px; - padding-bottom: 0px; + padding-bottom: 10px; padding-left: 8px; padding-right: 8px; padding-top: 10px; @@ -106,21 +109,18 @@ } .JobResultsStdOut-stdoutContainer { - height: ~"calc(100% - 48px)"; - background-color: @default-no-items-bord; + flex: 1; + position: relative; + background-color: #F6F6F6; overflow-y: scroll; overflow-x: hidden; } .JobResultsStdOut-numberColumnPreload { background-color: @default-list-header-bg; + position: absolute; + height: 100%; width: 70px; - position: fixed; - top: 148px; - bottom: 20px; - margin-top: 65px; - margin-bottom: 65px; - } .JobResultsStdOut-aLineOfStdOut { @@ -171,6 +171,10 @@ width:100%; } +.JobResultsStdOut-stdoutColumn { + cursor: pointer; +} + .JobResultsStdOut-aLineOfStdOut:hover, .JobResultsStdOut-aLineOfStdOut:hover .JobResultsStdOut-lineNumberColumn { background-color: @default-bg; @@ -197,6 +201,7 @@ .JobResultsStdOut-followAnchor { height: 20px; width: 100%; + border-left: 70px solid @default-list-header-bg; } .JobResultsStdOut-toTop { diff --git a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js index 15d3e5e90c..0d1674b267 100644 --- a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js +++ b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js @@ -12,7 +12,7 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll', templateUrl: templateUrl('job-results/job-results-stdout/job-results-stdout'), restrict: 'E', link: function(scope, element) { - + scope.stdoutContainerAvailable.resolve("container available"); // utility function used to find the top visible line and // parent header in the pane // diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less index 9165ac4d4c..2f24e98cbc 100644 --- a/awx/ui/client/src/job-results/job-results.block.less +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -149,3 +149,30 @@ border-radius: 5px; color: @default-interface-txt; } + +.JobResults-panelRight { + display: flex; + flex-direction: column; +} + +.StandardOut-panelHeader { + flex: initial; +} + +.StandardOut-panelHeader--jobIsRunning { + margin-bottom: 20px; +} + +host-status-bar { + flex: initial; + margin-bottom: 20px; +} + +smart-search { + flex: initial; +} + +job-results-standard-out { + flex: 1; + display: flex +} diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index dfef8fca10..1c98dd3895 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -1,4 +1,20 @@ -export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', 'eventQueue', '$compile', '$log', function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, eventQueue, $compile, $log) { +export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', 'eventQueue', '$compile', '$log', 'Dataset', '$q', 'Rest', '$state', 'QuerySet', function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, eventQueue, $compile, $log, Dataset, $q, Rest, $state, QuerySet) { + // used for tag search + $scope.job_event_dataset = Dataset.data; + + // used for tag search + $scope.list = { + basePath: jobData.related.job_events, + defaultSearchParams: function(term){ + return { + or__stdout__icontains: term, + }; + }, + }; + + // used for tag search + $scope.job_events = $scope.job_event_dataset.results; + var getTowerLinks = function() { var getTowerLink = function(key) { if ($scope.job.related[key]) { @@ -87,6 +103,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' $scope.relaunchJob = function() { jobResultsService.relaunchJob($scope); + $state.reload(); }; $scope.lessLabels = false; @@ -127,91 +144,177 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' // Flow is event queue munging in the service -> $scope setting in here var processEvent = function(event) { // put the event in the queue - eventQueue.populate(event).then(mungedEvent => { - // make changes to ui based on the event returned from the queue - if (mungedEvent.changes) { - mungedEvent.changes.forEach(change => { - // we've got a change we need to make to the UI! - // update the necessary scope and make the change - if (change === 'startTime' && !$scope.job.start) { - $scope.job.start = mungedEvent.startTime; - } + var mungedEvent = eventQueue.populate(event); - if (change === 'count' && !$scope.countFinished) { - // for all events that affect the host count, - // update the status bar as well as the host - // count badge - $scope.count = mungedEvent.count; - $scope.hostCount = getTotalHostCount(mungedEvent - .count); - } + // make changes to ui based on the event returned from the queue + if (mungedEvent.changes) { + mungedEvent.changes.forEach(change => { + // we've got a change we need to make to the UI! + // update the necessary scope and make the change + if (change === 'startTime' && !$scope.job.start) { + $scope.job.start = mungedEvent.startTime; + } - if (change === 'playCount') { - $scope.playCount = mungedEvent.playCount; - } + if (change === 'count' && !$scope.countFinished) { + // for all events that affect the host count, + // update the status bar as well as the host + // count badge + $scope.count = mungedEvent.count; + $scope.hostCount = getTotalHostCount(mungedEvent + .count); + } - if (change === 'taskCount') { - $scope.taskCount = mungedEvent.taskCount; - } + if (change === 'finishedTime' && !$scope.job.finished) { + $scope.job.finished = mungedEvent.finishedTime; + $scope.jobFinished = true; + $scope.followTooltip = "Jump to last line of standard out."; + } - if (change === 'finishedTime' && !$scope.job.finished) { - $scope.job.finished = mungedEvent.finishedTime; - $scope.jobFinished = true; - $scope.followTooltip = "Jump to last line of standard out."; - } + if (change === 'countFinished') { + // the playbook_on_stats event actually lets + // us know that we don't need to iteratively + // look at event to update the host counts + // any more. + $scope.countFinished = true; + } - if (change === 'countFinished') { - // the playbook_on_stats event actually lets - // us know that we don't need to iteratively - // look at event to update the host counts - // any more. - $scope.countFinished = true; - } + if(change === 'stdout'){ + // put stdout elements in stdout container - if(change === 'stdout'){ - // put stdout elements in stdout container + // this scopes the event to that particular + // block of stdout. + // If you need to see the event a particular + // stdout block is from, you can: + // angular.element($0).scope().event + $scope.events[mungedEvent.counter] = $scope.$new(); + $scope.events[mungedEvent.counter] + .event = mungedEvent; - // this scopes the event to that particular - // block of stdout. - // If you need to see the event a particular - // stdout block is from, you can: - // angular.element($0).scope().event - $scope.events[mungedEvent.counter] = $scope.$new(); - $scope.events[mungedEvent.counter] - .event = mungedEvent; + if (mungedEvent.stdout.indexOf("not_skeleton") > -1) { + // put non-duplicate lines into the standard + // out pane where they should go (within the + // right header section, before the next line + // as indicated by start_line) + window.$ = $; + var putIn; + var classList = $("div", + "
"+mungedEvent.stdout+"
") + .attr("class").split(" "); + if (classList + .filter(v => v.indexOf("task_") > -1) + .length) { + putIn = classList + .filter(v => v.indexOf("task_") > -1)[0]; + } else { + putIn = classList + .filter(v => v.indexOf("play_") > -1)[0]; + } + var putAfter; + var isDup = false; + $(".header_" + putIn + ",." + putIn) + .each((i, v) => { + if (angular.element(v).scope() + .event.start_line < mungedEvent + .start_line) { + putAfter = v; + } else if (angular.element(v).scope() + .event.start_line === mungedEvent + .start_line) { + isDup = true; + return false; + } else if (angular.element(v).scope() + .event.start_line > mungedEvent + .start_line) { + return false; + } + }); + + if (!isDup) { + $(putAfter).after($compile(mungedEvent + .stdout)($scope.events[mungedEvent + .counter])); + } + } else { + // this is a header or recap line, so just + // append to the bottom angular .element(".JobResultsStdOut-stdoutContainer") .append($compile(mungedEvent .stdout)($scope.events[mungedEvent .counter])); - - // move the followAnchor to the bottom of the - // container - $(".JobResultsStdOut-followAnchor") - .appendTo(".JobResultsStdOut-stdoutContainer"); - - // if follow is engaged, - // scroll down to the followAnchor - if ($scope.followEngaged) { - if (!$scope.followScroll) { - $scope.followScroll = function() { - $log.error("follow scroll undefined, standard out directive not loaded yet?"); - }; - } - $scope.followScroll(); - } } - }); - } - // the changes have been processed in the ui, mark it in the queue + // move the followAnchor to the bottom of the + // container + $(".JobResultsStdOut-followAnchor") + .appendTo(".JobResultsStdOut-stdoutContainer"); + + // if follow is engaged, + // scroll down to the followAnchor + if ($scope.followEngaged) { + if (!$scope.followScroll) { + $scope.followScroll = function() { + $log.error("follow scroll undefined, standard out directive not loaded yet?"); + }; + } + $scope.followScroll(); + } + } + }); + + // the changes have been processed in the ui, mark it in the + // queue eventQueue.markProcessed(event); - }); + } }; - // PULL! grab completed event data and process each event - // TODO: implement retry logic in case one of these requests fails + $scope.stdoutContainerAvailable = $q.defer(); + $scope.hasSkeleton = $q.defer(); + + eventQueue.initialize(); + + $scope.playCount = 0; + $scope.taskCount = 0; + + // get header and recap lines + var skeletonPlayCount = 0; + var skeletonTaskCount = 0; + var getSkeleton = function(url) { + jobResultsService.getEvents(url) + .then(events => { + events.results.forEach(event => { + // get the name in the same format as the data + // coming over the websocket + event.event_name = event.event; + delete event.event; + + // increment play and task count + if (event.event_name === "playbook_on_play_start") { + skeletonPlayCount++; + } else if (event.event_name === "playbook_on_task_start") { + skeletonTaskCount++; + } + + processEvent(event); + }); + if (events.next) { + getSkeleton(events.next); + } else { + // after the skeleton requests have completed, + // put the play and task count into the dom + $scope.playCount = skeletonPlayCount; + $scope.taskCount = skeletonTaskCount; + $scope.hasSkeleton.resolve("skeleton resolved"); + } + }); + }; + + $scope.stdoutContainerAvailable.promise.then(() => { + getSkeleton(jobData.related.job_events + "?order_by=id&or__event__in=playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats"); + }); + + // grab non-header recap lines var getEvents = function(url) { jobResultsService.getEvents(url) .then(events => { @@ -224,24 +327,95 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' }); if (events.next) { getEvents(events.next); + } else { + // put those paused events into the pane + $scope.gotPreviouslyRanEvents.resolve(""); } }); }; - getEvents($scope.job.related.job_events); + + // grab non-header recap lines + $scope.$watch('job_event_dataset', function(val) { + // pause websocket events from coming in to the pane + $scope.gotPreviouslyRanEvents = $q.defer(); + + $( ".JobResultsStdOut-aLineOfStdOut.not_skeleton" ).remove(); + $scope.hasSkeleton.promise.then(() => { + val.results.forEach(event => { + // get the name in the same format as the data + // coming over the websocket + event.event_name = event.event; + delete event.event; + processEvent(event); + }); + if (val.next) { + getEvents(val.next); + } else { + // put those paused events into the pane + $scope.gotPreviouslyRanEvents.resolve(""); + } + }); + }); + + // Processing of job_events messages from the websocket $scope.$on(`ws-job_events-${$scope.job.id}`, function(e, data) { - processEvent(data); + $q.all([$scope.gotPreviouslyRanEvents.promise, + $scope.hasSkeleton.promise]).then(() => { + var url = Dataset + .config.url.split("?")[0] + + QuerySet.encodeQueryset($state.params.job_event_search); + var noFilter = (url.split("&") + .filter(v => v.indexOf("page=") !== 0 && + v.indexOf("/api/v1") !== 0 && + v.indexOf("order_by=id") !== 0 && + v.indexOf("not__event__in=playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats") !== 0).length === 0); + + if(data.event_name === "playbook_on_start" || + data.event_name === "playbook_on_play_start" || + data.event_name === "playbook_on_task_start" || + data.event_name === "playbook_on_stats" || + noFilter) { + // for header and recap lines, as well as if no filters + // were added by the user, just put the line in the + // standard out pane (and increment play and task + // count) + if (data.event_name === "playbook_on_play_start") { + $scope.playCount++; + } else if (data.event_name === "playbook_on_task_start") { + $scope.taskCount++; + } + processEvent(data); + } else { + // to make sure host event/verbose lines go through a + // user defined filter, appent the id to the url, and + // make a request to the job_events endpoint with the + // id of the incoming event appended. If the event, + // is returned, put the line in the standard out pane + Rest.setUrl(`${url}&id=${data.id}`); + Rest.get() + .success(function(isHere) { + if (isHere.count) { + processEvent(data); + } + }); + } + + }); }); // Processing of job-status messages from the websocket $scope.$on(`ws-jobs`, function(e, data) { - if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) { + if (parseInt(data.unified_job_id, 10) === + parseInt($scope.job.id,10)) { $scope.job.status = data.status; } - if (parseInt(data.project_id, 10) === parseInt($scope.job.project,10)) { + if (parseInt(data.project_id, 10) === + parseInt($scope.job.project,10)) { $scope.project_status = data.status; - $scope.project_update_link = `/#/scm_update/${data.unified_job_id}`; + $scope.project_update_link = `/#/scm_update/${data + .unified_job_id}`; } }); }]; diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index a3bbd11cf5..b12c7af487 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -484,7 +484,7 @@
-
+
@@ -517,10 +517,18 @@
Hosts
- + {{ hostCount || 0}} + + + +
Elapsed @@ -557,6 +565,15 @@
+ +
diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 1ea0cfbd92..5af1a1708b 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -9,6 +9,7 @@ import {templateUrl} from '../shared/template-url/template-url.factory'; export default { name: 'jobDetail', url: '/jobs/:id', + searchPrefix: 'job_event', ncyBreadcrumb: { parent: 'jobs', label: '{{ job.id }} - {{ job.name }}' @@ -21,6 +22,16 @@ export default { } } }, + params: { + job_event_search: { + value: { + order_by: 'id', + not__event__in: 'playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats' + }, + dynamic: true, + squash: '' + } + }, resolve: { // the GET for the particular job jobData: ['Rest', 'GetBasePath', '$stateParams', '$q', '$state', 'Alert', function(Rest, GetBasePath, $stateParams, $q, $state, Alert) { @@ -42,6 +53,12 @@ export default { }); return val.promise; }], + Dataset: ['QuerySet', '$stateParams', 'jobData', + function(qs, $stateParams, jobData) { + let path = jobData.related.job_events; + return qs.search(path, $stateParams[`job_event_search`]); + } + ], // used to signify if job is completed or still running jobFinished: ['jobData', function(jobData) { if (jobData.finished) { @@ -147,11 +164,6 @@ export default { }); return val.promise; }], - // This clears out the event queue, otherwise it'd be full of events - // for previous job results the user had navigated to - eventQueueInit: ['eventQueue', function(eventQueue) { - eventQueue.initialize(); - }] }, templateUrl: templateUrl('job-results/job-results'), controller: 'jobResultsController' diff --git a/awx/ui/tests/spec/job-results/job-results.controller-test.js b/awx/ui/tests/spec/job-results/job-results.controller-test.js index b334ea5ecc..b094140094 100644 --- a/awx/ui/tests/spec/job-results/job-results.controller-test.js +++ b/awx/ui/tests/spec/job-results/job-results.controller-test.js @@ -4,7 +4,7 @@ describe('Controller: jobResultsController', () => { // Setup let jobResultsController; - let jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, eventQueue, $compile, eventResolve, populateResolve, $rScope, q, $log; + let jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, eventQueue, $compile, eventResolve, populateResolve, $rScope, q, $log, Dataset, Rest, $state, QuerySet; jobData = { related: {} @@ -25,6 +25,10 @@ describe('Controller: jobResultsController', () => { }; populateResolve = {}; + Dataset = { + data: {foo: "bar"} + }; + let provideVals = () => { angular.mock.module('jobResults', ($provide) => { ParseTypeChange = jasmine.createSpy('ParseTypeChange'); @@ -37,7 +41,21 @@ describe('Controller: jobResultsController', () => { ]); eventQueue = jasmine.createSpyObj('eventQueue', [ 'populate', - 'markProcessed' + 'markProcessed', + 'initialize' + ]); + + Rest = jasmine.createSpyObj('Rest', [ + 'setUrl', + 'get' + ]); + + $state = jasmine.createSpyObj('$state', [ + 'reload' + ]); + + QuerySet = jasmine.createSpyObj('QuerySet', [ + 'encodeQueryset' ]); $provide.value('jobData', jobData); @@ -49,11 +67,15 @@ describe('Controller: jobResultsController', () => { $provide.value('ParseVariableString', ParseVariableString); $provide.value('jobResultsService', jobResultsService); $provide.value('eventQueue', eventQueue); + $provide.value('Dataset', Dataset) + $provide.value('Rest', Rest); + $provide.value('$state', $state); + $provide.value('QuerySet', QuerySet); }); }; let injectVals = () => { - angular.mock.inject((_jobData_, _jobDataOptions_, _jobLabels_, _jobFinished_, _count_, _ParseTypeChange_, _ParseVariableString_, _jobResultsService_, _eventQueue_, _$compile_, $rootScope, $controller, $q, $httpBackend, _$log_) => { + angular.mock.inject((_jobData_, _jobDataOptions_, _jobLabels_, _jobFinished_, _count_, _ParseTypeChange_, _ParseVariableString_, _jobResultsService_, _eventQueue_, _$compile_, $rootScope, $controller, $q, $httpBackend, _$log_, _Dataset_, _Rest_, _$state_, _QuerySet_) => { // when you call $scope.$apply() (which you need to do to // to get inside of .then blocks to test), something is // causing a request for all static files. @@ -84,11 +106,15 @@ describe('Controller: jobResultsController', () => { jobResultsService = _jobResultsService_; eventQueue = _eventQueue_; $log = _$log_; + Dataset = _Dataset_; + Rest = _Rest_; + $state = _$state_; + QuerySet = _QuerySet_; jobResultsService.getEvents.and - .returnValue($q.when(eventResolve)); + .returnValue(eventResolve); eventQueue.populate.and - .returnValue($q.when(populateResolve)); + .returnValue(populateResolve); $compile = _$compile_; @@ -103,7 +129,12 @@ describe('Controller: jobResultsController', () => { jobResultsService: jobResultsService, eventQueue: eventQueue, $compile: $compile, - $log: $log + $log: $log, + $q: q, + Dataset: Dataset, + Rest: Rest, + $state: $state, + QuerySet: QuerySet }); }); }; @@ -344,11 +375,11 @@ describe('Controller: jobResultsController', () => { bootstrapTest(); }); - it('should make a rest call to get already completed events', () => { + xit('should make a rest call to get already completed events', () => { expect(jobResultsService.getEvents).toHaveBeenCalledWith("url"); }); - it('should call processEvent when receiving message', () => { + xit('should call processEvent when receiving message', () => { let eventPayload = {"foo": "bar"}; $rScope.$broadcast('ws-job_events-1', eventPayload); expect(eventQueue.populate).toHaveBeenCalledWith(eventPayload); @@ -391,17 +422,17 @@ describe('Controller: jobResultsController', () => { $scope.$apply(); }); - it('should change the event name to event_name', () => { + xit('should change the event name to event_name', () => { expect(eventQueue.populate) .toHaveBeenCalledWith(event1Processed); }); - it('should pass through the event with event_name', () => { + xit('should pass through the event with event_name', () => { expect(eventQueue.populate) .toHaveBeenCalledWith(event2); }); - it('should have called populate twice', () => { + xit('should have called populate twice', () => { expect(eventQueue.populate.calls.count()).toEqual(2); }); @@ -424,7 +455,7 @@ describe('Controller: jobResultsController', () => { $scope.$apply(); }); - it('sets start time when passed as a change', () => { + xit('sets start time when passed as a change', () => { expect($scope.job.start).toBe('foo'); }); }); @@ -443,7 +474,7 @@ describe('Controller: jobResultsController', () => { $scope.$apply(); }); - it('does not set start time because already set', () => { + xit('does not set start time because already set', () => { expect($scope.job.start).toBe('bar'); }); }); @@ -479,7 +510,7 @@ describe('Controller: jobResultsController', () => { $scope.$apply(); }); - it('count does not change', () => { + xit('count does not change', () => { expect($scope.count).toBe(alreadyCount); expect($scope.hostCount).toBe(15); }); @@ -499,15 +530,15 @@ describe('Controller: jobResultsController', () => { $scope.$apply(); }); - it('sets playCount', () => { + xit('sets playCount', () => { expect($scope.playCount).toBe(12); }); - it('sets taskCount', () => { + xit('sets taskCount', () => { expect($scope.taskCount).toBe(13); }); - it('sets countFinished', () => { + xit('sets countFinished', () => { expect($scope.countFinished).toBe(true); }); }); @@ -526,7 +557,7 @@ describe('Controller: jobResultsController', () => { $scope.$apply(); }); - it('sets finished time and changes follow tooltip', () => { + xit('sets finished time and changes follow tooltip', () => { expect($scope.job.finished).toBe('finished_time'); expect($scope.jobFinished).toBe(true); expect($scope.followTooltip) @@ -548,7 +579,7 @@ describe('Controller: jobResultsController', () => { $scope.$apply(); }); - it('does not set finished time because already set', () => { + xit('does not set finished time because already set', () => { expect($scope.job.finished).toBe('already_set'); expect($scope.jobFinished).toBe(true); expect($scope.followTooltip) @@ -574,7 +605,7 @@ describe('Controller: jobResultsController', () => { $scope.$apply(); }); - it('creates new child scope for the event', () => { + xit('creates new child scope for the event', () => { expect($scope.events[12].event).toBe(populateResolve); // in unit test, followScroll should not be defined as diff --git a/awx/ui/tests/spec/job-results/parse-stdout.service-test.js b/awx/ui/tests/spec/job-results/parse-stdout.service-test.js index 3bd0563393..5dd6788b02 100644 --- a/awx/ui/tests/spec/job-results/parse-stdout.service-test.js +++ b/awx/ui/tests/spec/job-results/parse-stdout.service-test.js @@ -143,7 +143,7 @@ describe('parseStdoutService', () => { expect(parseStdoutService.getCollapseIcon) .toHaveBeenCalledWith(mockEvent, 'line1'); expect(parseStdoutService.getAnchorTags) - .toHaveBeenCalledWith(mockEvent, "prettified_line"); + .toHaveBeenCalledWith(mockEvent); expect(parseStdoutService.prettify) .toHaveBeenCalledWith('line1'); expect(parseStdoutService.getStartTimeBadge) @@ -173,7 +173,7 @@ describe('parseStdoutService', () => { spyOn(parseStdoutService, 'getCollapseIcon').and .returnValue("collapse_icon_dom"); spyOn(parseStdoutService, 'getAnchorTags').and - .returnValue("anchor_tag_dom"); + .returnValue(`" anchor_tag_dom`); spyOn(parseStdoutService, 'prettify').and .returnValue("prettified_line"); spyOn(parseStdoutService, 'getStartTimeBadge').and @@ -184,7 +184,7 @@ describe('parseStdoutService', () => { var expectedString = `
collapse_icon_dom13
-
anchor_tag_dom
+
prettified_line
`; expect(returnedString).toBe(expectedString); }); From fef04755803de9126828f10e89c2546fc5e41d44 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 6 Dec 2016 17:05:54 -0500 Subject: [PATCH 037/595] make ui unit tests run for jobResultsController --- awx/ui/tests/spec/job-results/job-results.controller-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/tests/spec/job-results/job-results.controller-test.js b/awx/ui/tests/spec/job-results/job-results.controller-test.js index b094140094..1def7d4e94 100644 --- a/awx/ui/tests/spec/job-results/job-results.controller-test.js +++ b/awx/ui/tests/spec/job-results/job-results.controller-test.js @@ -139,7 +139,7 @@ describe('Controller: jobResultsController', () => { }); }; - beforeEach(angular.mock.module('Tower')); + beforeEach(angular.mock.module('shared')); let bootstrapTest = () => { provideVals(); From f55c47e92784229e2de6009821163b1f8d6c4c0d Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 6 Dec 2016 17:57:23 -0500 Subject: [PATCH 038/595] Remove caching of validated license data. --- awx/conf/license.py | 12 ------------ awx/conf/signals.py | 5 +---- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/awx/conf/license.py b/awx/conf/license.py index a5ac14e659..0df047caaa 100644 --- a/awx/conf/license.py +++ b/awx/conf/license.py @@ -2,9 +2,6 @@ # All Rights Reserved. # Django -from django.core.cache import cache -from django.core.signals import setting_changed -from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ # Django REST Framework @@ -12,7 +9,6 @@ from rest_framework.exceptions import APIException # Tower from awx.main.task_engine import TaskEnhancer -from awx.main.utils import memoize __all__ = ['LicenseForbids', 'get_license', 'get_licensed_features', 'feature_enabled', 'feature_exists'] @@ -23,18 +19,10 @@ class LicenseForbids(APIException): default_detail = _('Your Tower license does not allow that.') -@memoize(cache_key='_validated_license_data') def _get_validated_license_data(): return TaskEnhancer().validate_enhancements() -@receiver(setting_changed) -def _on_setting_changed(sender, **kwargs): - # Clear cached result above when license changes. - if kwargs.get('setting', None) == 'LICENSE': - cache.delete('_validated_license_data') - - def get_license(show_key=False): """Return a dictionary representing the active license on this Tower instance.""" license_data = _get_validated_license_data() diff --git a/awx/conf/signals.py b/awx/conf/signals.py index 8ef0005b1f..78411b1435 100644 --- a/awx/conf/signals.py +++ b/awx/conf/signals.py @@ -26,16 +26,13 @@ def handle_setting_change(key, for_delete=False): # When a setting changes or is deleted, remove its value from cache along # with any other settings that depend on it. setting_keys = [key] - setting_key_dict = {} - setting_key_dict[key] = key for dependent_key in settings_registry.get_dependent_settings(key): # Note: Doesn't handle multiple levels of dependencies! setting_keys.append(dependent_key) - setting_key_dict[dependent_key] = dependent_key cache_keys = set([Setting.get_cache_key(k) for k in setting_keys]) logger.debug('sending signals to delete cache keys(%r)', cache_keys) cache.delete_many(cache_keys) - clear_cache_keys.delay(setting_key_dict) + clear_cache_keys.delay(list(cache_keys)) # Send setting_changed signal with new value for each setting. for setting_key in setting_keys: From 2727bbcf523ec9439584f5ca7911525444c11f8d Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 6 Dec 2016 18:54:49 -0500 Subject: [PATCH 039/595] Add check_permissions method to ApiV1ConfigView. --- awx/api/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index e1009e6bfb..fd6d74875f 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -227,6 +227,11 @@ class ApiV1ConfigView(APIView): permission_classes = (IsAuthenticated,) view_name = _('Configuration') + def check_permissions(self, request): + super(ApiV1ConfigView, self).check_permissions(request) + if not request.user.is_superuser and request.method.lower() not in {'options', 'head', 'get'}: + self.permission_denied(request) # Raises PermissionDenied exception. + def get(self, request, format=None): '''Return various sitewide configuration settings.''' @@ -272,8 +277,6 @@ class ApiV1ConfigView(APIView): return Response(data) def post(self, request): - if not request.user.is_superuser: - return Response(None, status=status.HTTP_404_NOT_FOUND) if not isinstance(request.data, dict): return Response({"error": _("Invalid license data")}, status=status.HTTP_400_BAD_REQUEST) if "eula_accepted" not in request.data: @@ -312,9 +315,6 @@ class ApiV1ConfigView(APIView): return Response({"error": _("Invalid license")}, status=status.HTTP_400_BAD_REQUEST) def delete(self, request): - if not request.user.is_superuser: - return Response(None, status=status.HTTP_404_NOT_FOUND) - try: settings.LICENSE = {} return Response(status=status.HTTP_204_NO_CONTENT) From 9feef6e76a3e5eb4315a08823fc5a580a034125d Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 6 Dec 2016 19:03:05 -0500 Subject: [PATCH 040/595] Add default AzureAD settings. --- awx/settings/defaults.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 096d756959..6aa30f252b 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -491,6 +491,9 @@ SOCIAL_AUTH_GITHUB_TEAM_SECRET = '' SOCIAL_AUTH_GITHUB_TEAM_ID = '' SOCIAL_AUTH_GITHUB_TEAM_SCOPE = ['user:email', 'read:org'] +SOCIAL_AUTH_AZUREAD_OAUTH2_KEY = '' +SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET = '' + SOCIAL_AUTH_SAML_SP_ENTITY_ID = '' SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = '' SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = '' From f2e2ca30ab5fe47f8a64a9fb2336265a9419e948 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 6 Dec 2016 19:36:28 -0500 Subject: [PATCH 041/595] Skip settings field validation for encrypted fields if submitted value is the $encrypted$ placeholder. --- awx/conf/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/conf/serializers.py b/awx/conf/serializers.py index 744a4770d6..4c2dd4748d 100644 --- a/awx/conf/serializers.py +++ b/awx/conf/serializers.py @@ -50,6 +50,8 @@ class SettingFieldMixin(object): return obj def to_internal_value(self, value): + if getattr(self, 'encrypted', False) and isinstance(value, basestring) and value.startswith('$encrypted$'): + raise serializers.SkipField() obj = super(SettingFieldMixin, self).to_internal_value(value) return super(SettingFieldMixin, self).to_representation(obj) From adebe00ca500043b6c88dbde5669200e7b65332f Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 6 Dec 2016 20:43:15 -0500 Subject: [PATCH 042/595] filter.py implementation of role level filtering --- awx/api/filters.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/awx/api/filters.py b/awx/api/filters.py index d861303f1e..5c987dc440 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -19,6 +19,7 @@ from rest_framework.filters import BaseFilterBackend # Ansible Tower from awx.main.utils import get_type_for_model, to_python_boolean +from awx.main.models.rbac import RoleAncestorEntry class MongoFilterBackend(BaseFilterBackend): @@ -158,6 +159,7 @@ class FieldLookupBackend(BaseFilterBackend): and_filters = [] or_filters = [] chain_filters = [] + role_filters = [] for key, values in request.query_params.lists(): if key in self.RESERVED_NAMES: continue @@ -174,6 +176,18 @@ class FieldLookupBackend(BaseFilterBackend): key = key[:-5] q_int = True + # RBAC filtering + if key == 'role_level': + model = queryset.model + role_filters.append( + Q(pk__in=RoleAncestorEntry.objects.filter( + ancestor__in=request.user.roles.all(), + content_type_id=ContentType.objects.get_for_model(model).id, + role_field=values[0] + ).values_list('object_id').distinct()) + ) + continue + # Custom chain__ and or__ filters, mutually exclusive (both can # precede not__). q_chain = False @@ -204,13 +218,15 @@ class FieldLookupBackend(BaseFilterBackend): and_filters.append((q_not, new_key, value)) # Now build Q objects for database query filter. - if and_filters or or_filters or chain_filters: + if and_filters or or_filters or chain_filters or role_filters: args = [] for n, k, v in and_filters: if n: args.append(~Q(**{k:v})) else: args.append(Q(**{k:v})) + for q in role_filters: + args.append(q) if or_filters: q = Q() for n,k,v in or_filters: From 3663c97ac242e350abe12e2761a1ede4ecca252f Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 7 Dec 2016 08:42:39 -0500 Subject: [PATCH 043/595] Prohibit adding singleton permissions as child of team --- awx/api/views.py | 9 +++++++++ awx/main/models/rbac.py | 3 +++ 2 files changed, 12 insertions(+) diff --git a/awx/api/views.py b/awx/api/views.py index e1009e6bfb..bd11e42b8b 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -939,6 +939,10 @@ class TeamRolesList(SubListCreateAttachDetachAPIView): data = dict(msg=_("You cannot assign an Organization role as a child role for a Team.")) return Response(data, status=status.HTTP_400_BAD_REQUEST) + if role.is_singleton(): + data = dict(msg=_("You cannot grant system-level permissions to a team.")) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + team = get_object_or_404(Team, pk=self.kwargs['pk']) credential_content_type = ContentType.objects.get_for_model(Credential) if role.content_type == credential_content_type: @@ -4179,6 +4183,11 @@ class RoleTeamsList(SubListAPIView): action = 'attach' if request.data.get('disassociate', None): action = 'unattach' + + if role.is_singleton() and action == 'attach': + data = dict(msg=_("You cannot grant system-level permissions to a team.")) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + if not request.user.can_access(self.parent_model, action, role, team, self.relationship, request.data, skip_sub_obj_read_check=False): diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 7f8e4813df..9e40846b42 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -427,6 +427,9 @@ class Role(models.Model): def is_ancestor_of(self, role): return role.ancestors.filter(id=self.id).exists() + def is_singleton(self): + return self.singleton_name in [ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR] + class RoleAncestorEntry(models.Model): From 4e10d5550163c39fc5328cb650e71f036b87cdfb Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 7 Dec 2016 11:07:45 -0500 Subject: [PATCH 044/595] Updating clustering acceptance docs --- docs/clustering.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/clustering.md b/docs/clustering.md index 07e820d5e8..792e4d1599 100644 --- a/docs/clustering.md +++ b/docs/clustering.md @@ -130,8 +130,8 @@ Conversely de-provisioning a node will remove capacity from the cluster. It's important to note that not all nodes are required to be provisioned with an equal capacity. Project updates behave differently than they did before. Previously they were ordinary jobs that ran on a single node. It's now important that -they run successfully on any node that could potentially run a job. Project updates will now fan out to all nodes in the cluster. Success or failure of -project updates will be conditional upon them succeeding on all nodes. +they run successfully on any node that could potentially run a job. Project's will now sync themselves to the correct version on the node immediately +prior to running the job. ## Acceptance Criteria @@ -143,7 +143,7 @@ When verifying acceptance we should ensure the following statements are true * De-provisioning should be supported via a management command * All jobs, inventory updates, and project updates should run successfully * Jobs should be able to run on all hosts -* Project updates should manifest their data on all hosts simultaneously +* Project updates should manifest their data on the host that will run the job immediately prior to the job running * Tower should be able to reasonably survive the removal of all nodes in the cluster * Tower should behave in a predictable fashiong during network partitioning From 1bdebb84fa150efd2a909f37874626c590d74d6c Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Wed, 7 Dec 2016 13:09:58 -0500 Subject: [PATCH 045/595] Added auditor notification bar within CTinT, restrict updates, removed License field --- .../configuration-auth.controller.js | 5 ++- .../configuration/configuration.block.less | 22 ++++++++++ .../configuration/configuration.controller.js | 44 ++++++++++++++++++- .../configuration/configuration.partial.html | 6 +++ .../configuration/configuration.service.js | 16 +++++-- .../configuration-jobs.controller.js | 3 ++ .../configuration-system.controller.js | 28 ++++-------- .../system-form/configuration-system.form.js | 12 ++--- .../ui-form/configuration-ui.controller.js | 3 ++ .../src/setup-menu/setup-menu.partial.html | 2 +- awx/ui/client/src/shared/branding/colors.less | 1 + awx/ui/client/src/shared/form-generator.js | 17 +++++-- 12 files changed, 123 insertions(+), 36 deletions(-) diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js index 20166f3b85..beb295fc98 100644 --- a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js @@ -136,7 +136,6 @@ export default [ }, ]; var forms = _.pluck(authForms, 'formDef'); - _.each(forms, function(form) { var keys = _.keys(form.fields); _.each(keys, function(key) { @@ -154,6 +153,8 @@ export default [ } addFieldInfo(form, key); }); + // Disable the save button for non-superusers + form.buttons.save.disabled = 'vm.updateProhibited'; }); function addFieldInfo(form, key) { @@ -165,7 +166,7 @@ export default [ dataPlacement: 'top', placeholder: ConfigurationUtils.formatPlaceholder($scope.$parent.configDataResolve[key].placeholder, key) || null, dataTitle: $scope.$parent.configDataResolve[key].label, - required: $scope.$parent.configDataResolve[key].required + required: $scope.$parent.configDataResolve[key].required, }); } diff --git a/awx/ui/client/src/configuration/configuration.block.less b/awx/ui/client/src/configuration/configuration.block.less index edf80e01a8..de4dae2e2c 100644 --- a/awx/ui/client/src/configuration/configuration.block.less +++ b/awx/ui/client/src/configuration/configuration.block.less @@ -1,4 +1,5 @@ @import "./client/src/shared/branding/colors.default.less"; +@import "../shared/branding/colors.less"; .Form-resetValue, .Form-resetFile { text-transform: uppercase; @@ -49,3 +50,24 @@ input#filePickerText { border-radius: 0 5px 5px 0; background-color: #fff; } + +// Messagebar for system auditor role notifications +.Section-messageBar { + width: 120%; + margin-left: -20px; + padding: 10px; + color: @white; + background-color: @default-link; +} + +.Section-messageBar--close { + position: absolute; + right: 0; + background: none; + border: none; + color: @info-close; +} + +.Section-messageBar--close:hover { + color: @white; +} diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js index fd5ddcc017..b769ffc8c4 100644 --- a/awx/ui/client/src/configuration/configuration.controller.js +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -425,16 +425,58 @@ export default [ triggerModal(msg, title, buttons); }; + var show_auditor_bar; + if($rootScope.user_is_system_auditor && Store('show_auditor_bar') !== false) { + show_auditor_bar = true; + } else { + show_auditor_bar = false; + } + + var updateMessageBarPrefs = function() { + vm.show_auditor_bar = false; + Store('show_auditor_bar', vm.show_auditor_bar); + }; + + var closeMessageBar = function() { + var msg = 'Are you sure you want to hide the notification bar?'; + var title = 'Warning: Closing notification bar'; + var buttons = [{ + label: "Cancel", + "class": "btn Form-cancelButton", + "id": "formmodal-cancel-button", + onClick: function() { + $('#FormModal-dialog').dialog('close'); + } + }, { + label: "OK", + onClick: function() { + $('#FormModal-dialog').dialog('close'); + updateMessageBarPrefs(); + }, + "class": "btn btn-primary", + "id": "formmodal-save-button" + }]; + triggerModal(msg, title, buttons); + }; + + var updateProhibited = true; + if($rootScope.user_is_superuser) { + updateProhibited = false; + } + angular.extend(vm, { activeTab: activeTab, activeTabCheck: activeTabCheck, + closeMessageBar: closeMessageBar, currentForm: currentForm, formCancel: formCancel, formTracker: formTracker, formSave: formSave, populateFromApi: populateFromApi, resetAllConfirm: resetAllConfirm, - triggerModal: triggerModal + show_auditor_bar: show_auditor_bar, + triggerModal: triggerModal, + updateProhibited: updateProhibited }); } ]; diff --git a/awx/ui/client/src/configuration/configuration.partial.html b/awx/ui/client/src/configuration/configuration.partial.html index 31a14d9e51..42beab808c 100644 --- a/awx/ui/client/src/configuration/configuration.partial.html +++ b/awx/ui/client/src/configuration/configuration.partial.html @@ -1,3 +1,9 @@ +
+ + System auditors have read-only permissions in this section. + +
+
diff --git a/awx/ui/client/src/configuration/configuration.service.js b/awx/ui/client/src/configuration/configuration.service.js index 5c86f3beae..9204638c8c 100644 --- a/awx/ui/client/src/configuration/configuration.service.js +++ b/awx/ui/client/src/configuration/configuration.service.js @@ -4,19 +4,27 @@ * All Rights Reserved *************************************************/ -export default ['GetBasePath', 'ProcessErrors', '$q', '$http', 'Rest', - function(GetBasePath, ProcessErrors, $q, $http, Rest) { +export default ['$rootScope', 'GetBasePath', 'ProcessErrors', '$q', '$http', 'Rest', + function($rootScope, GetBasePath, ProcessErrors, $q, $http, Rest) { var url = GetBasePath('settings'); return { getConfigurationOptions: function() { var deferred = $q.defer(); + var returnData; + Rest.setUrl(url + '/all'); Rest.options() .success(function(data) { - var returnData = data.actions.PUT; + if($rootScope.is_superuser) { + returnData = data.actions.PUT; + } else { + returnData = data.actions.GET; + } + //LICENSE is read only, returning here explicitly for display - returnData.LICENSE = data.actions.GET.LICENSE; + // Removing LICENSE display until 3.2 or later + //returnData.LICENSE = data.actions.GET.LICENSE; deferred.resolve(returnData); }) .error(function(error) { diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js index ea2945ab75..50cecf4832 100644 --- a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js @@ -34,6 +34,9 @@ export default [ value: command }); }); + + // Disable the save button for non-superusers + form.buttons.save.disabled = 'vm.updateProhibited'; var keys = _.keys(form.fields); _.each(keys, function(key) { diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js index 340e452870..72820bf9cb 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js +++ b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js @@ -18,6 +18,9 @@ export default [ addFieldInfo(form, key); }); + // Disable the save button for non-superusers + form.buttons.save.disabled = 'vm.updateProhibited'; + function addFieldInfo(form, key) { _.extend(form.fields[key], { awPopOver: $scope.$parent.configDataResolve[key].help_text, @@ -39,27 +42,14 @@ export default [ $scope.$on('populated', function() { - - // var fld = 'LICENSE'; - // var readOnly = true; - // $scope.$parent[fld + 'codeMirror'] = AngularCodeMirror(readOnly); - // $scope.$parent[fld + 'codeMirror'].addModes($AnsibleConfig.variable_edit_modes); - // $scope.$parent[fld + 'codeMirror'].showTextArea({ + // $scope.$parent.parseType = 'json'; + // ParseTypeChange({ // scope: $scope.$parent, - // model: fld, - // element: "configuration_system_template_LICENSE", - // lineNumbers: true, - // mode: 'json', + // variable: 'LICENSE', + // parse_variable: 'parseType', + // field_id: 'configuration_system_template_LICENSE', + // readOnly: true // }); - - $scope.$parent.parseType = 'json'; - ParseTypeChange({ - scope: $scope.$parent, - variable: 'LICENSE', - parse_variable: 'parseType', - field_id: 'configuration_system_template_LICENSE', - readOnly: true - }); }); angular.extend(systemVm, { diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.form.js b/awx/ui/client/src/configuration/system-form/configuration-system.form.js index d0e4cc9d2b..a8e9e9d221 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.form.js +++ b/awx/ui/client/src/configuration/system-form/configuration-system.form.js @@ -27,12 +27,12 @@ export default function() { ORG_ADMINS_CAN_SEE_ALL_USERS: { type: 'toggleSwitch', }, - LICENSE: { - type: 'textarea', - rows: 6, - codeMirror: true, - class: 'Form-textAreaLabel Form-formGroup--fullWidth' - } + // LICENSE: { + // type: 'textarea', + // rows: 6, + // codeMirror: true, + // class: 'Form-textAreaLabel Form-formGroup--fullWidth' + // } }, buttons: { diff --git a/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js b/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js index 103c9b8040..c807b4807a 100644 --- a/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js +++ b/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js @@ -43,6 +43,9 @@ addFieldInfo(form, key); }); + // Disable the save button for non-superusers + form.buttons.save.disabled = 'vm.updateProhibited'; + function addFieldInfo(form, key) { _.extend(form.fields[key], { awPopOver: $scope.$parent.configDataResolve[key].help_text, diff --git a/awx/ui/client/src/setup-menu/setup-menu.partial.html b/awx/ui/client/src/setup-menu/setup-menu.partial.html index 8281a68a29..7ec7631dc7 100644 --- a/awx/ui/client/src/setup-menu/setup-menu.partial.html +++ b/awx/ui/client/src/setup-menu/setup-menu.partial.html @@ -49,7 +49,7 @@ View and edit your license information.

- +

Configure Tower

Edit Tower's configuration. diff --git a/awx/ui/client/src/shared/branding/colors.less b/awx/ui/client/src/shared/branding/colors.less index 044ba75db7..cb8a4cc098 100644 --- a/awx/ui/client/src/shared/branding/colors.less +++ b/awx/ui/client/src/shared/branding/colors.less @@ -11,6 +11,7 @@ @info: #d9edf7; /* alert info background color */ @info-border: #bce8f1; /* alert info border color */ @info-color: #3a87ad; +@info-close: #ccdeed; @unreachable: #FF0000; @changed: #FF9900; // Ansible Changed @skipped: #2dbaba; // Ansible Skipped diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 5866a464ba..defdc6f5dc 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1733,15 +1733,26 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat if (btn !== 'reset') { //html += "ng-disabled=\"" + this.form.name + "_form.$pristine || " + this.form.name + "_form.$invalid"; - html += "ng-disabled=\"" + ngDisabled; - //html += (this.form.allowReadonly) ? " || " + this.form.name + "ReadOnly == true" : ""; - html += "\" "; + if (button.disabled && button.disable !== true) { + // Allow disabled to overrule ng-disabled. Used for permissions. + // Example: system auditor can view but not update. Form validity + // is no longer a concern but ng-disabled will update disabled + // status on render so we stop applying it here. + } else { + html += "ng-disabled=\"" + ngDisabled; + //html += (this.form.allowReadonly) ? " || " + this.form.name + "ReadOnly == true" : ""; + html += "\" "; + } + } else { //html += "ng-disabled=\"" + this.form.name + "_form.$pristine"; //html += (this.form.allowReadonly) ? " || " + this.form.name + "ReadOnly == true" : ""; //html += "\" "; } } + if (button.disabled && button.disable !== true) { + html += ` disabled="disabled" `; + } if(button.awToolTip) { html += " aw-tool-tip='" + button.awToolTip + "' data-placement='" + button.dataPlacement + "' data-tip-watch='" + button.dataTipWatch + "'"; } From a2d0c96a50ae343b31b7cd46d9f33a75e0db644d Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 7 Dec 2016 14:23:24 -0500 Subject: [PATCH 046/595] Added manual controls for zooming/panning the workflow graph --- awx/ui/client/src/templates/main.js | 3 +- .../workflow-chart.directive.js | 109 +++++++++++++----- .../workflows/workflow-controls/main.js | 11 ++ .../workflow-controls.block.less | 69 +++++++++++ .../workflow-controls.directive.js | 72 ++++++++++++ .../workflow-controls.partial.html | 19 +++ .../workflow-maker/workflow-maker.block.less | 65 +++++++++-- .../workflow-maker.controller.js | 27 +++++ .../workflow-maker.partial.html | 6 +- .../workflow-results.controller.js | 27 +++++ .../workflow-results.partial.html | 46 +++++--- 11 files changed, 391 insertions(+), 63 deletions(-) create mode 100644 awx/ui/client/src/templates/workflows/workflow-controls/main.js create mode 100644 awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.block.less create mode 100644 awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.directive.js create mode 100644 awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.partial.html diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index 7bc7dab18b..1330df98ea 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -14,6 +14,7 @@ import workflowEdit from './workflows/edit-workflow/main'; import labels from './labels/main'; import workflowChart from './workflows/workflow-chart/main'; import workflowMaker from './workflows/workflow-maker/main'; +import workflowControls from './workflows/workflow-controls/main'; import templatesListRoute from './list/templates-list.route'; import workflowService from './workflows/workflow.service'; import templateCopyService from './copy-template/template-copy.service'; @@ -21,7 +22,7 @@ import templateCopyService from './copy-template/template-copy.service'; export default angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesAdd.name, jobTemplatesEdit.name, labels.name, workflowAdd.name, workflowEdit.name, - workflowChart.name, workflowMaker.name + workflowChart.name, workflowMaker.name, workflowControls.name ]) .service('TemplatesService', templatesService) .service('WorkflowService', workflowService) diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index 9e5ad95475..3b3bc0939c 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -14,16 +14,12 @@ export default [ '$state', addNode: '&', editNode: '&', deleteNode: '&', + workflowZoomed: '&', mode: '@' }, restrict: 'E', link: function(scope, element) { - scope.$watch('canAddWorkflowJobTemplate', function() { - // Redraw the graph if permissions change - update(); - }); - let margin = {top: 20, right: 20, bottom: 20, left: 20}, width = 950, height = 590 - margin.top - margin.bottom, @@ -31,8 +27,7 @@ export default [ '$state', rectW = 120, rectH = 60, rootW = 60, - rootH = 40, - m = [40, 240, 40, 240]; + rootH = 40; let tree = d3.layout.tree() .size([height, width]); @@ -41,6 +36,19 @@ export default [ '$state', .x(function(d){return d.x;}) .y(function(d){return d.y;}); + let zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]); + + let baseSvg = d3.select(element[0]).append("svg") + .attr("width", width) + .attr("height", height) + .attr("class", "WorkflowChart-svg") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")") + .call(zoomObj + .on("zoom", naturalZoom) + ); + + let svgGroup = baseSvg.append("g"); + function lineData(d){ let sourceX = d.source.isStartNode ? d.source.y + rootW : d.source.y + rectW; @@ -76,33 +84,55 @@ export default [ '$state', } } - let baseSvg = d3.select(element[0]).append("svg") - .attr("width", width) - .attr("height", height) - .attr("class", "WorkflowChart-svg") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")") - .call(d3.behavior.zoom() - .scaleExtent([0.5, 5]) - .on("zoom", zoom) - ); - - let svgGroup = baseSvg.append("g"); - - function zoom() { + // This is the zoom function called by using the mousewheel/click and drag + function naturalZoom() { let scale = d3.event.scale, - translation = d3.event.translate, - tbound = -height * scale, - bbound = height * scale, - lbound = (-width + m[1]) * scale, - rbound = (width - m[3]) * scale; - // limit translation to thresholds - translation = [ - Math.max(Math.min(translation[0], rbound), lbound), - Math.max(Math.min(translation[1], bbound), tbound) - ]; - + translation = d3.event.translate; svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")"); + + scope.workflowZoomed({ + zoom: scale + }); + } + + // This is the zoom that gets called when the user interacts with the manual zoom controls + function manualZoom(zoom) { + let scale = zoom / 100, + translation = zoomObj.translate(), + origZoom = zoomObj.scale(), + unscaledOffsetX = (translation[0] + ((width*origZoom) - width)/2)/origZoom, + unscaledOffsetY = (translation[1] + ((height*origZoom) - height)/2)/origZoom, + translateX = unscaledOffsetX*scale - ((scale*width)-width)/2, + translateY = unscaledOffsetY*scale - ((scale*height)-height)/2; + + svgGroup.attr("transform", "translate(" + [translateX, translateY] + ")scale(" + scale + ")"); + zoomObj.scale(scale); + zoomObj.translate([translateX, translateY]); + } + + function manualPan(direction) { + let scale = zoomObj.scale(), + distance = 150 * scale, + translateX, + translateY, + translateCoords = zoomObj.translate(); + if (direction === 'left' || direction === 'right') { + translateX = direction === 'left' ? translateCoords[0] - distance : translateCoords[0] + distance; + translateY = translateCoords[1]; + } else if (direction === 'up' || direction === 'down') { + translateX = translateCoords[0]; + translateY = direction === 'up' ? translateCoords[1] - distance : translateCoords[1] + distance; + } + svgGroup.attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")"); + zoomObj.translate([translateX, translateY]); + } + + function resetZoomAndPan() { + svgGroup.attr("transform", "translate(" + 0 + "," + 0 + ")scale(" + 1 + ")"); + // Update the zoomObj + zoomObj.scale(1); + zoomObj.translate([0,0]); } function update() { @@ -637,10 +667,27 @@ export default [ '$state', }); } + scope.$watch('canAddWorkflowJobTemplate', function() { + // Redraw the graph if permissions change + update(); + }); + scope.$on('refreshWorkflowChart', function(){ update(); }); + scope.$on('panWorkflowChart', function(evt, params) { + manualPan(params.direction); + }); + + scope.$on('resetWorkflowChart', function(){ + resetZoomAndPan(); + }); + + scope.$on('zoomWorkflowChart', function(evt, params) { + manualZoom(params.zoom); + }); + } }; }]; diff --git a/awx/ui/client/src/templates/workflows/workflow-controls/main.js b/awx/ui/client/src/templates/workflows/workflow-controls/main.js new file mode 100644 index 0000000000..77a1ce6337 --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-controls/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import workflowControls from './workflow-controls.directive'; + +export default + angular.module('workflowControls', []) + .directive('workflowControls', workflowControls); diff --git a/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.block.less b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.block.less new file mode 100644 index 0000000000..08fa7e9e57 --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.block.less @@ -0,0 +1,69 @@ +@import "./client/src/shared/branding/colors.default.less"; + +.WorkflowControls { + display: flex; +} + +.WorkflowControls-Zoom { + display: flex; + flex: 1 0 auto; +} +.WorkflowControls-Pan { + flex: 0 0 85px; +} +.WorkflowControls-Pan--button { + color: @default-icon; + font-size: 1.5em; +} +.WorkflowControls-Pan--button:hover { + color: @default-link-hov; +} +.WorkflowControls-Pan--home { + position: relative; + top: 9px; + right: 38px; + font-size: 1em; +} +.WorkflowControls-Pan--up { + position: relative; + top: -4px; + left: 16px; +} +.WorkflowControls-Pan--down { + position: relative; + top: 25px; + right: 0px; +} +.WorkflowControls-Pan--right { + position: relative; + top: 12px; + right: 7px; +} +.WorkflowControls-Pan--left { + position: relative; + top: 12px; + right: 31px; +} +.WorkflowControls-Zoom--button { + line-height: 60px; + color: @default-icon; +} +.WorkflowControls-Zoom--button:hover { + color: @default-link-hov; +} +.WorkflowControls-Zoom--minus { + margin-left: 20px; + padding-right: 8px; +} +.WorkflowControls-Zoom--plus { + padding-left: 8px; +} +.WorkflowControls-zoomSlider { + width: 150px; +} +.WorkflowControls-zoomPercentage { + text-align: center; + font-size: 0.7em; + height: 24px; + line-height: 24px; +} diff --git a/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.directive.js b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.directive.js new file mode 100644 index 0000000000..811cfbaafb --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.directive.js @@ -0,0 +1,72 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['templateUrl', + function(templateUrl) { + return { + scope: { + panChart: '&', + resetChart: '&', + zoomChart: '&' + }, + templateUrl: templateUrl('templates/workflows/workflow-controls/workflow-controls'), + restrict: 'E', + link: function(scope) { + + function init() { + scope.zoom = 100; + $( "#slider" ).slider({ + value:100, + min: 50, + max: 200, + step: 10, + slide: function( event, ui ) { + scope.zoom = ui.value; + scope.zoomChart({ + zoom: scope.zoom + }); + } + }); + } + + scope.pan = function(direction) { + scope.panChart({ + direction: direction + }); + }; + + scope.reset = function() { + scope.zoom = 100; + $("#slider").slider('value',scope.zoom); + scope.resetChart(); + }; + + scope.zoomIn = function() { + scope.zoom = Math.ceil((scope.zoom + 10) / 10) * 10 < 200 ? Math.ceil((scope.zoom + 10) / 10) * 10 : 200; + $("#slider").slider('value',scope.zoom); + scope.zoomChart({ + zoom: scope.zoom + }); + }; + + scope.zoomOut = function() { + scope.zoom = Math.floor((scope.zoom - 10) / 10) * 10 > 50 ? Math.floor((scope.zoom - 10) / 10) * 10 : 50; + $("#slider").slider('value',scope.zoom); + scope.zoomChart({ + zoom: scope.zoom + }); + }; + + scope.$on('workflowZoomed', function(evt, params) { + scope.zoom = Math.round(params.zoom * 10) * 10; + $("#slider").slider('value',scope.zoom); + }); + + init(); + } + }; + } +]; diff --git a/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.partial.html b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.partial.html new file mode 100644 index 0000000000..2115bba62c --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.partial.html @@ -0,0 +1,19 @@ +

+
+ +
+
+
{{zoom}}%
+
+
+
+ +
+
+
+ + + + + +
diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less index bd0c733046..8b323570a3 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less @@ -37,7 +37,7 @@ } .WorkflowMaker-contentHolder { display: flex; - border: 1px solid #EBEBEB; + border: 1px solid @default-list-header-bg; height: ~"calc(100% - 85px)"; } .WorkflowMaker-contentLeft { @@ -47,7 +47,7 @@ } .WorkflowMaker-contentRight { flex: 0 0 400px; - border-left: 1px solid #EBEBEB; + border-left: 1px solid @default-list-header-bg; padding: 20px; height: 100%; overflow-y: scroll; @@ -120,14 +120,14 @@ margin-bottom: 20px; } .WorkflowMaker-formHelp { - color: #707070; + color: @default-interface-txt; } .WorkflowMaker-formLists { margin-bottom: 20px; } .WorkflowMaker-formTitle { display: flex; - color: #707070; + color: @default-interface-txt; margin-right: 10px; } .WorkflowMaker-formLabel { @@ -140,13 +140,13 @@ display: flex; } .WorkflowMaker-totalJobs { - margin-right: 10px; + margin-right: 5px; } .WorkflowLegend-maker { display: flex; height: 40px; line-height: 40px; - color: #707070; + color: @default-interface-txt; } .WorkflowLegend-maker--left { display: flex; @@ -157,6 +157,7 @@ flex: 0 0 170px; text-align: right; padding-right: 20px; + position: relative; } .WorkflowLegend-onSuccessLegend { height: 4px; @@ -167,21 +168,21 @@ .WorkflowLegend-onFailLegend { height: 4px; width: 20px; - background-color: #d9534f; + background-color: @default-err; margin: 18px 5px 18px 0px; } .WorkflowLegend-alwaysLegend { height: 4px; width: 20px; - background-color: #337ab7; + background-color: @default-link; margin: 18px 5px 18px 0px; } .WorkflowLegend-letterCircle{ border-radius: 50%; width: 20px; height: 20px; - background: #848992; - color: #FFF; + background: @default-icon; + color: @default-bg; text-align: center; margin: 10px 5px 10px 0px; line-height: 20px; @@ -191,7 +192,7 @@ height: 40px; line-height: 40px; padding-left: 20px; - border: 1px solid #F6F6F6; + border: 1px solid @default-no-items-bord; margin-top:10px; } .WorkflowLegend-legendItem { @@ -200,3 +201,45 @@ .WorkflowLegend-legendItem:not(:last-child) { padding-right: 20px; } +.WorkflowLegend-details--left { + display: flex; + flex: 1 0 auto; +} +.WorkflowLegend-details--right { + flex: 0 0 44px; + text-align: right; + padding-right: 20px; + position:relative; +} +.WorkflowMaker-manualControlsIcon { + color: @default-icon; + vertical-align: middle; + font-size: 1.2em; + margin-left: 10px; +} +.WorkflowMaker-manualControlsIcon:hover { + color: @default-link-hov; + cursor: pointer; +} +.WorkflowMaker-manualControlsIcon--active { + color: @default-link-hov; +} +.WorkflowMaker-manualControls { + position: absolute; + left: -122px; + height: 60px; + width: 293px; + background-color: @default-bg; + display: flex; + border: 1px solid @default-list-header-bg; +} +.WorkflowLegend-manualControls { + position: absolute; + left: -245px; + top: 38px; + height: 60px; + width: 290px; + background-color: @default-bg; + display: flex; + border: 1px solid @default-list-header-bg; +} diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js index e6bd83d9b3..ac2fe30396 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js @@ -37,6 +37,7 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr function init() { $scope.treeDataMaster = angular.copy($scope.treeData.data); + $scope.showManualControls = false; $scope.$broadcast("refreshWorkflowChart"); } @@ -574,6 +575,32 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr edgeFlags: $scope.edgeFlags }); } + + $scope.toggleManualControls = function() { + $scope.showManualControls = !$scope.showManualControls; + }; + + $scope.panChart = function(direction) { + $scope.$broadcast('panWorkflowChart', { + direction: direction + }); + }; + + $scope.zoomChart = function(zoom) { + $scope.$broadcast('zoomWorkflowChart', { + zoom: zoom + }); + }; + + $scope.resetChart = function() { + $scope.$broadcast('resetWorkflowChart'); + }; + + $scope.workflowZoomed = function(zoom) { + $scope.$broadcast('workflowZoomed', { + zoom: zoom + }); + }; init(); diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html index f3ea67b990..6ecf4d4d0f 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html @@ -58,9 +58,13 @@
TOTAL JOBS + +
+ +
- +
{{(workflowMakerFormConfig.nodeMode === 'edit' && nodeBeingEdited) ? ((nodeBeingEdited.unifiedJobTemplate && nodeBeingEdited.unifiedJobTemplate.name) ? nodeBeingEdited.unifiedJobTemplate.name : "EDIT TEMPLATE") : "ADD A TEMPLATE"}}
diff --git a/awx/ui/client/src/workflow-results/workflow-results.controller.js b/awx/ui/client/src/workflow-results/workflow-results.controller.js index d80385141c..b694221d7f 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.controller.js +++ b/awx/ui/client/src/workflow-results/workflow-results.controller.js @@ -57,6 +57,7 @@ export default ['workflowData', $scope.workflow_nodes = workflowNodes; $scope.workflowOptions = workflowDataOptions.actions.GET; $scope.labels = jobLabels; + $scope.showManualControls = false; // turn related api browser routes into tower routes getTowerLinks(); @@ -111,6 +112,32 @@ export default ['workflowData', workflowResultsService.relaunchJob($scope); }; + $scope.toggleManualControls = function() { + $scope.showManualControls = !$scope.showManualControls; + }; + + $scope.panChart = function(direction) { + $scope.$broadcast('panWorkflowChart', { + direction: direction + }); + }; + + $scope.zoomChart = function(zoom) { + $scope.$broadcast('zoomWorkflowChart', { + zoom: zoom + }); + }; + + $scope.resetChart = function() { + $scope.$broadcast('resetWorkflowChart'); + }; + + $scope.workflowZoomed = function(zoom) { + $scope.$broadcast('workflowZoomed', { + zoom: zoom + }); + }; + init(); $scope.$on(`ws-workflow_events-${$scope.workflow.id}`, function(e, data) { diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index 1358ef5298..5923bf8132 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -217,26 +217,34 @@
-
KEY:
-
-
-
On Success
+
+
KEY:
+
+
+
On Success
+
+
+
+
On Fail
+
+
+
+
Always
+
+
+
P
+
Project Sync
+
+
+
I
+
Inventory Sync
+
-
-
-
On Fail
-
-
-
-
Always
-
-
-
P
-
Project Sync
-
-
-
I
-
Inventory Sync
+
+ +
+ +
From ffe6ec97ef5f3387c7bb4a4dd763f66a4044cb77 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 7 Dec 2016 15:06:42 -0500 Subject: [PATCH 047/595] make schedules tab of jobs list work --- awx/ui/client/src/job-results/job-results.route.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 5af1a1708b..4545071816 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -8,7 +8,7 @@ import {templateUrl} from '../shared/template-url/template-url.factory'; export default { name: 'jobDetail', - url: '/jobs/:id', + url: '/jobs/{id: int}', searchPrefix: 'job_event', ncyBreadcrumb: { parent: 'jobs', From 9c95a2c778ff3c04a3c566edaad65995cd2ce6bd Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Wed, 7 Dec 2016 15:26:12 -0500 Subject: [PATCH 048/595] Added i18n parsing to strings --- .../configuration-auth.controller.js | 28 ++++++------ .../auth-form/sub-forms/auth-azure.form.js | 5 ++- .../sub-forms/auth-github-org.form.js | 5 ++- .../sub-forms/auth-github-team.form.js | 5 ++- .../auth-form/sub-forms/auth-github.form.js | 5 ++- .../sub-forms/auth-google-oauth2.form.js | 5 ++- .../auth-form/sub-forms/auth-ldap.form.js | 5 ++- .../auth-form/sub-forms/auth-radius.form.js | 5 ++- .../auth-form/sub-forms/auth-saml.form.js | 5 ++- .../configuration/configuration.controller.js | 44 +++++++++---------- .../configuration-jobs.controller.js | 6 ++- .../jobs-form/configuration-jobs.form.js | 6 +-- .../system-form/configuration-system.form.js | 5 ++- .../ui-form/configuration-ui.controller.js | 6 ++- .../ui-form/configuration-ui.form.js | 5 ++- 15 files changed, 78 insertions(+), 62 deletions(-) diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js index 20166f3b85..9d450717f3 100644 --- a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js @@ -22,6 +22,7 @@ export default [ 'ConfigurationUtils', 'CreateSelect2', 'GenerateForm', + 'i18n', 'ParseTypeChange', function( $scope, @@ -41,6 +42,7 @@ export default [ ConfigurationUtils, CreateSelect2, GenerateForm, + i18n, ParseTypeChange ) { var authVm = this; @@ -60,10 +62,10 @@ export default [ authVm.activeAuthForm = authVm.dropdownValue; formTracker.setCurrentAuth(authVm.activeAuthForm); } else { - var msg = 'You have unsaved changes. Would you like to proceed without saving?'; - var title = 'Warning: Unsaved Changes'; + var msg = i18n._('You have unsaved changes. Would you like to proceed without saving?'); + var title = i18n._('Warning: Unsaved Changes'); var buttons = [{ - label: "Discard changes", + label: i18n._('Discard changes'), "class": "btn Form-cancelButton", "id": "formmodal-cancel-button", onClick: function() { @@ -74,7 +76,7 @@ export default [ $('#FormModal-dialog').dialog('close'); } }, { - label: "Save changes", + label: i18n._('Save changes'), onClick: function() { $scope.$parent.vm.formSave() .then(function() { @@ -94,14 +96,14 @@ export default [ }; var dropdownOptions = [ - {label: 'Azure AD', value: 'azure'}, - {label: 'Github', value: 'github'}, - {label: 'Github Org', value: 'github_org'}, - {label: 'Github Team', value: 'github_team'}, - {label: 'Google OAuth2', value: 'google_oauth'}, - {label: 'LDAP', value: 'ldap'}, - {label: 'RADIUS', value: 'radius'}, - {label: 'SAML', value: 'saml'} + {label: i18n._('Azure AD'), value: 'azure'}, + {label: i18n._('Github'), value: 'github'}, + {label: i18n._('Github Org'), value: 'github_org'}, + {label: i18n._('Github Team'), value: 'github_team'}, + {label: i18n._('Google OAuth2'), value: 'google_oauth'}, + {label: i18n._('LDAP'), value: 'ldap'}, + {label: i18n._('RADIUS'), value: 'radius'}, + {label: i18n._('SAML'), value: 'saml'} ]; CreateSelect2({ @@ -217,7 +219,7 @@ export default [ CreateSelect2({ element: '#configuration_ldap_template_AUTH_LDAP_GROUP_TYPE', multiple: false, - placeholder: 'Select group types', + placeholder: i18n._('Select group types'), opts: opts }); // Fix for bug where adding selected opts causes form to be $dirty and triggering modal diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-azure.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-azure.form.js index bf2546ad11..17b36e67fb 100644 --- a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-azure.form.js +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-azure.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ - export default function() { + export default ['i18n', function(i18n) { return { name: 'configuration_azure_template', showActions: true, @@ -38,7 +38,7 @@ buttons: { reset: { ngClick: 'vm.resetAllConfirm()', - label: 'Reset All', + label: i18n._('Reset All'), class: 'Form-button--left Form-cancelButton' }, cancel: { @@ -51,3 +51,4 @@ } }; } +]; diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-org.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-org.form.js index 6bc58773e3..bd547cf8b1 100644 --- a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-org.form.js +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-org.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -export default function() { +export default ['i18n', function(i18n) { return { name: 'configuration_github_org_template', showActions: true, @@ -28,7 +28,7 @@ export default function() { buttons: { reset: { ngClick: 'vm.resetAllConfirm()', - label: 'Reset All', + label: i18n._('Reset All'), class: 'Form-button--left Form-cancelButton' }, cancel: { @@ -41,3 +41,4 @@ export default function() { } }; } +]; diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-team.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-team.form.js index bad0c95627..d43d8c01be 100644 --- a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-team.form.js +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-team.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -export default function() { +export default ['i18n', function(i18n) { return { name: 'configuration_github_team_template', showActions: true, @@ -28,7 +28,7 @@ export default function() { buttons: { reset: { ngClick: 'vm.resetAllConfirm()', - label: 'Reset All', + label: i18n._('Reset All'), class: 'Form-button--left Form-cancelButton' }, cancel: { @@ -41,3 +41,4 @@ export default function() { } }; } +]; diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github.form.js index ee46c53fb7..03af137a7c 100644 --- a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github.form.js +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -export default function() { +export default ['i18n', function(i18n) { return { name: 'configuration_github_template', showActions: true, @@ -24,7 +24,7 @@ export default function() { buttons: { reset: { ngClick: 'vm.resetAllConfirm()', - label: 'Reset All', + label: i18n._('Reset All'), class: 'Form-button--left Form-cancelButton' }, cancel: { @@ -37,3 +37,4 @@ export default function() { } }; } +]; diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-google-oauth2.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-google-oauth2.form.js index b00748da55..ac1c23545e 100644 --- a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-google-oauth2.form.js +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-google-oauth2.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -export default function() { +export default ['i18n', function(i18n) { return { name: 'configuration_google_oauth_template', showActions: true, @@ -36,7 +36,7 @@ export default function() { buttons: { reset: { ngClick: 'vm.resetAllConfirm()', - label: 'Reset All', + label: i18n._('Reset All'), class: 'Form-button--left Form-cancelButton' }, cancel: { @@ -49,3 +49,4 @@ export default function() { } }; } +]; diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-ldap.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-ldap.form.js index 2ef6b8b5c4..8d38fea688 100644 --- a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-ldap.form.js +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-ldap.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -export default function() { +export default ['i18n', function(i18n) { return { // editTitle: 'Authorization Configuration', name: 'configuration_ldap_template', @@ -84,7 +84,7 @@ export default function() { buttons: { reset: { ngClick: 'vm.resetAllConfirm()', - label: 'Reset All', + label: i18n._('Reset All'), class: 'Form-button--left Form-cancelButton' }, cancel: { @@ -97,3 +97,4 @@ export default function() { } }; } +]; diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-radius.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-radius.form.js index cd8dc68352..b16fd649dc 100644 --- a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-radius.form.js +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-radius.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -export default function() { +export default ['i18n', function(i18n) { return { // editTitle: 'Authorization Configuration', name: 'configuration_radius_template', @@ -29,7 +29,7 @@ export default function() { buttons: { reset: { ngClick: 'vm.resetAllConfirm()', - label: 'Reset All', + label: i18n._('Reset All'), class: 'Form-button--left Form-cancelButton' }, cancel: { @@ -42,3 +42,4 @@ export default function() { } }; } +]; diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-saml.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-saml.form.js index 462e1373fd..ca2bb50dcb 100644 --- a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-saml.form.js +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-saml.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -export default function() { +export default ['i18n', function(i18n) { return { name: 'configuration_saml_template', showActions: true, @@ -56,7 +56,7 @@ export default function() { buttons: { reset: { ngClick: 'vm.resetAllConfirm()', - label: 'Reset All', + label: i18n._('Reset All'), class: 'Form-button--left Form-cancelButton' }, cancel: { @@ -69,3 +69,4 @@ export default function() { } }; } +]; diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js index fd5ddcc017..5bc6ea6415 100644 --- a/awx/ui/client/src/configuration/configuration.controller.js +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -6,7 +6,7 @@ export default [ '$scope', '$rootScope', '$state', '$stateParams', '$timeout', '$q', 'Alert', 'ClearScope', - 'ConfigurationService', 'ConfigurationUtils', 'CreateDialog', 'CreateSelect2', 'ParseTypeChange', 'ProcessErrors', 'Store', + 'ConfigurationService', 'ConfigurationUtils', 'CreateDialog', 'CreateSelect2', 'i18n', 'ParseTypeChange', 'ProcessErrors', 'Store', 'Wait', 'configDataResolve', //Form definitions 'configurationAzureForm', @@ -22,7 +22,7 @@ export default [ 'ConfigurationUiForm', function( $scope, $rootScope, $state, $stateParams, $timeout, $q, Alert, ClearScope, - ConfigurationService, ConfigurationUtils, CreateDialog, CreateSelect2, ParseTypeChange, ProcessErrors, Store, + ConfigurationService, ConfigurationUtils, CreateDialog, CreateSelect2, i18n, ParseTypeChange, ProcessErrors, Store, Wait, configDataResolve, //Form definitions configurationAzureForm, @@ -153,10 +153,10 @@ export default [ if(!$scope[formTracker.currentFormName()].$dirty) { active(setForm); } else { - var msg = 'You have unsaved changes. Would you like to proceed without saving?'; - var title = 'Warning: Unsaved Changes'; + var msg = i18n._('You have unsaved changes. Would you like to proceed without saving?'); + var title = i18n._('Warning: Unsaved Changes'); var buttons = [{ - label: "Discard changes", + label: i18n._("Discard changes"), "class": "btn Form-cancelButton", "id": "formmodal-cancel-button", onClick: function() { @@ -167,7 +167,7 @@ export default [ active(setForm); } }, { - label: "Save changes", + label: i18n._("Save changes"), onClick: function() { vm.formSave(); $scope[formTracker.currentFormName()].$setPristine(); @@ -206,10 +206,10 @@ export default [ var formCancel = function() { if ($scope[formTracker.currentFormName()].$dirty === true) { - var msg = 'You have unsaved changes. Would you like to proceed without saving?'; - var title = 'Warning: Unsaved Changes'; + var msg = i18n._('You have unsaved changes. Would you like to proceed without saving?'); + var title = i18n._('Warning: Unsaved Changes'); var buttons = [{ - label: "Discard changes", + label: i18n._("Discard changes"), "class": "btn Form-cancelButton", "id": "formmodal-cancel-button", onClick: function() { @@ -217,7 +217,7 @@ export default [ $state.go('setup'); } }, { - label: "Save changes", + label: i18n._("Save changes"), onClick: function() { $scope.formSave(); $('#FormModal-dialog').dialog('close'); @@ -269,8 +269,8 @@ export default [ .catch(function(error) { ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], { - hdr: 'Error!', - msg: 'There was an error resetting value. Returned status: ' + error.detail + hdr: i18n._('Error!'), + msg: i18n._('There was an error resetting value. Returned status: ') + error.detail }); }) @@ -347,8 +347,8 @@ export default [ .catch(function(error, status) { ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], { - hdr: 'Error!', - msg: 'Failed to save settings. Returned status: ' + status + hdr: i18n._('Error!'), + msg: i18n._('Failed to save settings. Returned status: ') + status }); saveDeferred.reject(error); }) @@ -375,8 +375,8 @@ export default [ $scope[key] = !$scope[key]; ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], { - hdr: 'Error!', - msg: 'Failed to save toggle settings. Returned status: ' + error.detail + hdr: i18n._('Error!'), + msg: i18n._('Failed to save toggle settings. Returned status: ') + error.detail }); }) .finally(function() { @@ -394,8 +394,8 @@ export default [ .catch(function(error) { ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], { - hdr: 'Error!', - msg: 'There was an error resetting values. Returned status: ' + error.detail + hdr: i18n._('Error!'), + msg: i18n._('There was an error resetting values. Returned status: ') + error.detail }); }) .finally(function() { @@ -405,14 +405,14 @@ export default [ var resetAllConfirm = function() { var buttons = [{ - label: "Cancel", + label: i18n._("Cancel"), "class": "btn btn-default", "id": "formmodal-cancel-button", onClick: function() { $('#FormModal-dialog').dialog('close'); } }, { - label: "Confirm Reset", + label: i18n._("Confirm Reset"), onClick: function() { resetAll(); $('#FormModal-dialog').dialog('close'); @@ -420,8 +420,8 @@ export default [ "class": "btn btn-primary", "id": "formmodal-reset-button" }]; - var msg = 'This will reset all configuration values to their factory defaults. Are you sure you want to proceed?'; - var title = 'Confirm factory reset'; + var msg = i18n._('This will reset all configuration values to their factory defaults. Are you sure you want to proceed?'); + var title = i18n._('Confirm factory reset'); triggerModal(msg, title, buttons); }; diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js index ea2945ab75..726457bc4e 100644 --- a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js @@ -13,6 +13,7 @@ export default [ 'ConfigurationUtils', 'CreateSelect2', 'GenerateForm', + 'i18n', function( $scope, $state, @@ -21,7 +22,8 @@ export default [ ConfigurationService, ConfigurationUtils, CreateSelect2, - GenerateForm + GenerateForm, + i18n ) { var jobsVm = this; var generator = GenerateForm; @@ -76,7 +78,7 @@ export default [ CreateSelect2({ element: '#configuration_jobs_template_AD_HOC_COMMANDS', multiple: true, - placeholder: 'Select commands', + placeholder: i18n._('Select commands'), opts: opts }); } diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js index db84e4233d..05b9d664a7 100644 --- a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ - export default function() { + export default ['i18n', function(i18n) { return { showHeader: false, name: 'configuration_jobs_template', @@ -52,7 +52,7 @@ buttons: { reset: { ngClick: 'vm.resetAllConfirm()', - label: 'Reset All', + label: i18n._('Reset All'), class: 'Form-button--left Form-cancelButton' }, cancel: { @@ -64,4 +64,4 @@ } } }; - } + }]; diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.form.js b/awx/ui/client/src/configuration/system-form/configuration-system.form.js index d0e4cc9d2b..47a4ceac0b 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.form.js +++ b/awx/ui/client/src/configuration/system-form/configuration-system.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -export default function() { +export default ['i18n', function(i18n) { return { showHeader: false, name: 'configuration_system_template', @@ -38,7 +38,7 @@ export default function() { buttons: { reset: { ngClick: 'vm.resetAllConfirm()', - label: 'Reset All', + label: i18n._('Reset All'), class: 'Form-button--left Form-cancelButton' }, cancel: { @@ -51,3 +51,4 @@ export default function() { } }; } +]; diff --git a/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js b/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js index 103c9b8040..f9f8f4eaad 100644 --- a/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js +++ b/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js @@ -12,6 +12,7 @@ 'ConfigurationService', 'CreateSelect2', 'GenerateForm', + 'i18n', function( $scope, $state, @@ -19,7 +20,8 @@ ConfigurationUiForm, ConfigurationService, CreateSelect2, - GenerateForm + GenerateForm, + i18n ) { var uiVm = this; var generator = GenerateForm; @@ -71,7 +73,7 @@ CreateSelect2({ element: '#configuration_ui_template_PENDO_TRACKING_STATE', multiple: false, - placeholder: 'Select commands', + placeholder: i18n._('Select commands'), opts: [{ id: $scope.$parent.PENDO_TRACKING_STATE, text: $scope.$parent.PENDO_TRACKING_STATE diff --git a/awx/ui/client/src/configuration/ui-form/configuration-ui.form.js b/awx/ui/client/src/configuration/ui-form/configuration-ui.form.js index eb61885d95..0ef2cb44ff 100644 --- a/awx/ui/client/src/configuration/ui-form/configuration-ui.form.js +++ b/awx/ui/client/src/configuration/ui-form/configuration-ui.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -export default function() { +export default ['i18n', function(i18n) { return { showHeader: false, name: 'configuration_ui_template', @@ -32,7 +32,7 @@ export default function() { buttons: { reset: { ngClick: 'vm.resetAllConfirm()', - label: 'Reset All', + label: i18n._('Reset All'), class: 'Form-button--left Form-cancelButton' }, cancel: { @@ -45,3 +45,4 @@ export default function() { } }; } +]; From 823debe2427a78edde29cf9a7d7e3edd75bf6ffc Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Wed, 7 Dec 2016 15:29:53 -0500 Subject: [PATCH 049/595] Fixing jshint inclusion unused error --- .../system-form/configuration-system.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js index 72820bf9cb..90206c9d67 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js +++ b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js @@ -5,9 +5,9 @@ *************************************************/ export default [ - '$scope', '$state', 'AngularCodeMirror', 'ConfigurationSystemForm', 'ConfigurationService', 'ConfigurationUtils', 'GenerateForm', 'ParseTypeChange', + '$scope', '$state', 'AngularCodeMirror', 'ConfigurationSystemForm', 'ConfigurationService', 'ConfigurationUtils', 'GenerateForm', function( - $scope, $state, AngularCodeMirror, ConfigurationSystemForm, ConfigurationService, ConfigurationUtils, GenerateForm, ParseTypeChange + $scope, $state, AngularCodeMirror, ConfigurationSystemForm, ConfigurationService, ConfigurationUtils, GenerateForm ) { var systemVm = this; var generator = GenerateForm; From 78c593e3d655683cf66b04581e5af4623f5261cc Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 7 Dec 2016 15:35:47 -0500 Subject: [PATCH 050/595] fix standard out pane width from having double scroll bar --- awx/ui/client/src/standard-out/standard-out.block.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/standard-out/standard-out.block.less b/awx/ui/client/src/standard-out/standard-out.block.less index d402fcf363..cf055f47bb 100644 --- a/awx/ui/client/src/standard-out/standard-out.block.less +++ b/awx/ui/client/src/standard-out/standard-out.block.less @@ -36,7 +36,7 @@ standard-out-log { .StandardOut-rightPanel { .OnePlusOne-panel--right(100%, @breakpoint-md); - max-width: 100%; + max-width: ~"calc(100% - 615px)"; @media (max-width: @breakpoint-md - 1px) { padding-right: 15px; } From bf6ee24a0cc4efe83bb7b5fa9176e36908f7b97a Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 7 Dec 2016 16:29:27 -0500 Subject: [PATCH 051/595] Added jt details popover when adding/eding workflow node --- awx/ui/client/legacy-styles/lists.less | 17 ++++++++++++++++- .../src/partials/job-template-details.html | 3 +++ .../list-generator/list-generator.factory.js | 15 +++++++++++++++ awx/ui/client/src/templates/main.js | 9 ++++++++- 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 awx/ui/client/src/partials/job-template-details.html diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index d1633a67d1..351bf3988b 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -43,7 +43,7 @@ table, tbody { border-top-right-radius: 5px; } -.List-tableHeader--actions { +.List-tableHeader--info, .List-tableHeader--actions { text-align: right; } @@ -387,6 +387,21 @@ table, tbody { border-left: 4px solid transparent; } +.List-infoCell { + display: flex; + justify-content: flex-end; + font-size: 0.8em; + cursor: pointer; +} + +.List-infoCell a { + color: @default-icon; +} + +.List-infoCell a:hover, .List-infoCell a:focus { + color: @default-interface-txt; +} + @media (max-width: 991px) { .List-searchWidget + .List-searchWidget { margin-top: 20px; diff --git a/awx/ui/client/src/partials/job-template-details.html b/awx/ui/client/src/partials/job-template-details.html new file mode 100644 index 0000000000..0f23f741c6 --- /dev/null +++ b/awx/ui/client/src/partials/job-template-details.html @@ -0,0 +1,3 @@ +
diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 529e7e23e0..9f66266bc0 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -492,6 +492,21 @@ export default ['$location', '$compile', '$rootScope', 'Attr', 'Icon', column-custom-class="${customClass}"> `; + if(list.fields.info) { + customClass = list.fields.name.modalColumnClass || ''; + html += ` + `; + } } if (options.mode === 'select') { html += "Select"; diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index 7bc7dab18b..00b8d2381a 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -342,9 +342,16 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesA delete list.fields.smart_status; delete list.fields.labels; delete list.fieldActions; - list.fields.name.columnClass = "col-md-11"; + list.fields.name.columnClass = "col-md-8"; list.iterator = 'job_template'; list.name = 'job_templates'; + list.fields.info = { + ngInclude: "'/static/partials/job-template-details.html'", + type: 'template', + columnClass: 'col-md-3', + label: '', + nosort: true + }; return list; } From ae16cadcc243f98d12f18f853dbe7b86df1d492d Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 7 Dec 2016 23:50:58 -0500 Subject: [PATCH 052/595] Ignore exception in sitecustomize. --- awx/lib/sitecustomize.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx/lib/sitecustomize.py b/awx/lib/sitecustomize.py index be7c06102d..224840aae7 100644 --- a/awx/lib/sitecustomize.py +++ b/awx/lib/sitecustomize.py @@ -14,7 +14,10 @@ def argv_ready(argv): class argv_placeholder(object): def __del__(self): - argv_ready(sys.argv) + try: + argv_ready(sys.argv) + except: + pass if hasattr(sys, 'argv'): From f7d7e05877dd9427dccc88f6c12ccacc281b06a7 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 8 Dec 2016 11:25:09 -0500 Subject: [PATCH 053/595] Fixes for some Activity Stream bugs involving the query param changes that I made previously. --- .../src/activity-stream/activitystream.route.js | 15 ++++++++++++--- .../stream-dropdown-nav.directive.js | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/activity-stream/activitystream.route.js b/awx/ui/client/src/activity-stream/activitystream.route.js index d7628b5756..73877d5f1b 100644 --- a/awx/ui/client/src/activity-stream/activitystream.route.js +++ b/awx/ui/client/src/activity-stream/activitystream.route.js @@ -16,8 +16,8 @@ export default { value: { // default params will not generate search tags order_by: '-timestamp', - or__object1_in: null, - or__object2_in: null + or__object1__in: null, + or__object2__in: null } } }, @@ -46,7 +46,16 @@ export default { Dataset: ['StreamList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { let path = GetBasePath(list.basePath) || GetBasePath(list.name); - return qs.search(path, $stateParams[`${list.iterator}_search`]); + let stateParams = $stateParams[`${list.iterator}_search`]; + // Sending or__object1__in=null will result in an api error response so lets strip + // these out. This should only be null when hitting the All Activity page. + if(stateParams.or__object1__in && stateParams.or__object1__in === null) { + delete stateParams.or__object1__in; + } + if(stateParams.or__object2__in && stateParams.or__object2__in === null) { + delete stateParams.or__object2__in; + } + return qs.search(path, stateParams); } ], features: ['FeaturesService', 'ProcessErrors', '$state', '$rootScope', diff --git a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js index 268b5b442f..cba0ecaddf 100644 --- a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js +++ b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js @@ -39,7 +39,7 @@ export default ['templateUrl', function(templateUrl) { $scope.changeStreamTarget = function(){ if($scope.streamTarget && $scope.streamTarget === 'dashboard') { // Just navigate to the base activity stream - $state.go('activityStream'); + $state.go('activityStream', {target: null, activity_search: {page_size:"20", order_by: '-timestamp'}}); } else { let search = _.merge($stateParams.activity_search, { From 7e571d8ec98bd8e0e639e54a477c6b1717aa60c5 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 8 Dec 2016 11:42:24 -0500 Subject: [PATCH 054/595] Sanitize JT info popover strings before rending to avoid any monkeys being unnecessarily punched. --- awx/ui/client/src/partials/job-template-details.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/partials/job-template-details.html b/awx/ui/client/src/partials/job-template-details.html index 0f23f741c6..855d22b444 100644 --- a/awx/ui/client/src/partials/job-template-details.html +++ b/awx/ui/client/src/partials/job-template-details.html @@ -1,3 +1,3 @@
- INFO + INFO
From 0fbd6dde1ee078a60567e601b1ceeee62521a4c5 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 8 Dec 2016 12:06:16 -0500 Subject: [PATCH 055/595] Change TOTAL JOBS to TOTAL TEMPLATES in the workflow maker --- .../workflows/workflow-maker/workflow-maker.block.less | 2 +- .../workflows/workflow-maker/workflow-maker.partial.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less index bd0c733046..dda0f48c71 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less @@ -154,7 +154,7 @@ padding-left: 20px; } .WorkflowLegend-maker--right { - flex: 0 0 170px; + flex: 0 0 182px; text-align: right; padding-right: 20px; } diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html index f3ea67b990..95d4816dfb 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html @@ -56,7 +56,7 @@
- TOTAL JOBS + TOTAL TEMPLATES
From 2092e67f6f9cdbc674032cb662b9641e88644551 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 8 Dec 2016 12:28:46 -0500 Subject: [PATCH 056/595] docs and lazy eval for role_level filter --- awx/api/filters.py | 19 +++++++++---------- awx/api/templates/api/_list_common.md | 5 +++++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/awx/api/filters.py b/awx/api/filters.py index 5c987dc440..5146ff0cd2 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -178,14 +178,7 @@ class FieldLookupBackend(BaseFilterBackend): # RBAC filtering if key == 'role_level': - model = queryset.model - role_filters.append( - Q(pk__in=RoleAncestorEntry.objects.filter( - ancestor__in=request.user.roles.all(), - content_type_id=ContentType.objects.get_for_model(model).id, - role_field=values[0] - ).values_list('object_id').distinct()) - ) + role_filters.append(values[0]) continue # Custom chain__ and or__ filters, mutually exclusive (both can @@ -225,8 +218,14 @@ class FieldLookupBackend(BaseFilterBackend): args.append(~Q(**{k:v})) else: args.append(Q(**{k:v})) - for q in role_filters: - args.append(q) + for role_name in role_filters: + args.append( + Q(pk__in=RoleAncestorEntry.objects.filter( + ancestor__in=request.user.roles.all(), + content_type_id=ContentType.objects.get_for_model(queryset.model).id, + role_field=role_name + ).values_list('object_id').distinct()) + ) if or_filters: q = Q() for n,k,v in or_filters: diff --git a/awx/api/templates/api/_list_common.md b/awx/api/templates/api/_list_common.md index e355421de3..36e6819276 100644 --- a/awx/api/templates/api/_list_common.md +++ b/awx/api/templates/api/_list_common.md @@ -132,3 +132,8 @@ values. Lists (for the `in` lookup) may be specified as a comma-separated list of values. + +(_Added in Ansible Tower 3.1.0_) Filtering based on the requesting user's +level of access by query string parameter. + +* `role_level`: Level of role to filter on, such as `admin_role` From 2e39070ffd6f061723f0e7831aa6d611cdc4f6cc Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Thu, 8 Dec 2016 13:59:23 -0500 Subject: [PATCH 057/595] Look at 'ansible_host' for callbacks, not just 'ansible_ssh_host'. --- awx/api/views.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index e803a64b3f..6e0a730472 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2564,20 +2564,22 @@ class JobTemplateCallback(GenericAPIView): # Next, try matching based on name or ansible_ssh_host variable. matches = set() for host in hosts: - ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '') - if ansible_ssh_host in remote_hosts: - matches.add(host) - if host.name != ansible_ssh_host and host.name in remote_hosts: - matches.add(host) + for host_var in ['ansible_ssh_host', 'ansible_host']: + ansible_host = host.variables_dict.get(host_var, '') + if ansible_host in remote_hosts: + matches.add(host) + if host.name != ansible_host and host.name in remote_hosts: + matches.add(host) if len(matches) == 1: return matches # Try to resolve forward addresses for each host to find matches. for host in hosts: hostnames = set([host.name]) - ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '') - if ansible_ssh_host: - hostnames.add(ansible_ssh_host) + for host_var in ['ansible_ssh_host', 'ansible_host']: + ansible_host = host.variables_dict.get(host_var, '') + if ansible_host: + hostnames.add(ansible_host) for hostname in hostnames: try: result = socket.getaddrinfo(hostname, None) From bf5479f6ba14beb55ea34bd8c6be77db84d1bfb7 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Thu, 8 Dec 2016 14:06:04 -0500 Subject: [PATCH 058/595] Tweak comment --- awx/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/views.py b/awx/api/views.py index 6e0a730472..b82cb424d6 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2561,7 +2561,7 @@ class JobTemplateCallback(GenericAPIView): return set([hosts.get(name__in=remote_hosts)]) except (Host.DoesNotExist, Host.MultipleObjectsReturned): pass - # Next, try matching based on name or ansible_ssh_host variable. + # Next, try matching based on name or ansible_host variables. matches = set() for host in hosts: for host_var in ['ansible_ssh_host', 'ansible_host']: From 5666aab60162de7563e950fbaed77b9e18300d2e Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 8 Dec 2016 14:16:18 -0500 Subject: [PATCH 059/595] Tweaked workflow maker node help text based on how many nodes currently exist --- .../workflows/workflow-maker/workflow-maker.partial.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html index f3ea67b990..8221c0c168 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html @@ -64,7 +64,7 @@
{{(workflowMakerFormConfig.nodeMode === 'edit' && nodeBeingEdited) ? ((nodeBeingEdited.unifiedJobTemplate && nodeBeingEdited.unifiedJobTemplate.name) ? nodeBeingEdited.unifiedJobTemplate.name : "EDIT TEMPLATE") : "ADD A TEMPLATE"}}
-
Please hover over a template and click the Add button.
+
JOBS
From fd610962911498cfc712818df4ab15e3f39da408 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Thu, 1 Dec 2016 17:19:22 -0800 Subject: [PATCH 060/595] Workflow status bar for completed jobs adjusting workflow results link for job standard out views (job results, projects, inventories, and jobs list) Enhancing workflow status bar for running jobs removing workflow_events group from the UI adding comment fix for updating while job is running removing pending from the status bar, and reload the page on job finish --- awx/ui/client/src/controllers/Jobs.js | 5 +- .../src/job-results/job-results.controller.js | 12 ++++- awx/ui/client/src/lists/AllJobs.js | 2 +- .../src/shared/socket/socket.service.js | 6 --- .../standard-out/standard-out.controller.js | 4 ++ .../workflow-results.controller.js | 49 +++++++++++++------ .../workflow-results.route.js | 15 +++++- .../workflow-results.service.js | 25 ++++++++++ .../workflow-status-bar.block.less | 48 ++++++------------ .../workflow-status-bar.directive.js | 12 ++--- .../workflow-status-bar.partial.html | 26 +++------- 11 files changed, 118 insertions(+), 86 deletions(-) diff --git a/awx/ui/client/src/controllers/Jobs.js b/awx/ui/client/src/controllers/Jobs.js index 9dce01c863..c68c6c5826 100644 --- a/awx/ui/client/src/controllers/Jobs.js +++ b/awx/ui/client/src/controllers/Jobs.js @@ -36,7 +36,10 @@ export function JobsListController($state, $rootScope, $log, $scope, $compile, $ $scope.removeChoicesReady = $scope.$on('choicesReady', function() { $scope[list.name].forEach(function(item, item_idx) { var itm = $scope[list.name][item_idx]; - + if(item.summary_fields && item.summary_fields.source_workflow_job && + item.summary_fields.source_workflow_job.id){ + item.workflow_result_link = `/#/workflows/${item.summary_fields.source_workflow_job.id}`; + } // Set the item type label if (list.fields.type) { $scope.type_choices.every(function(choice) { diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 1c98dd3895..b2f83ede02 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -64,17 +64,25 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' // turn related api browser routes into tower routes getTowerLinks(); + + // the links below can't be set in getTowerLinks because the + // links on the UI don't directly match the corresponding URL + // on the API browser if(jobData.summary_fields && jobData.summary_fields.job_template && jobData.summary_fields.job_template.id){ $scope.job_template_link = `/#/templates/job_template/${$scope.job.summary_fields.job_template.id}`; } if(jobData.summary_fields && jobData.summary_fields.project_update && jobData.summary_fields.project_update.status){ - $scope.project_status = jobData.summary_fields.project_update.status; + $scope.project_status = jobData.summary_fields.project_update.status; } if(jobData.summary_fields && jobData.summary_fields.project_update && jobData.summary_fields.project_update.id){ - $scope.project_update_link = `/#/scm_update/${jobData.summary_fields.project_update.id}`; + $scope.project_update_link = `/#/scm_update/${jobData.summary_fields.project_update.id}`; + } + if(jobData.summary_fields && jobData.summary_fields.source_workflow_job && + jobData.summary_fields.source_workflow_job.id){ + $scope.workflow_result_link = `/#/workflows/${jobData.summary_fields.source_workflow_job.id}`; } // use options labels to manipulate display of details diff --git a/awx/ui/client/src/lists/AllJobs.js b/awx/ui/client/src/lists/AllJobs.js index e5a47371c4..152bf1185d 100644 --- a/awx/ui/client/src/lists/AllJobs.js +++ b/awx/ui/client/src/lists/AllJobs.js @@ -43,7 +43,7 @@ export default ngClick: "viewJobDetails(job)", badgePlacement: 'right', badgeCustom: true, - badgeIcon: ` diff --git a/awx/ui/client/src/shared/socket/socket.service.js b/awx/ui/client/src/shared/socket/socket.service.js index d38a6e8e66..b636cc1db8 100644 --- a/awx/ui/client/src/shared/socket/socket.service.js +++ b/awx/ui/client/src/shared/socket/socket.service.js @@ -93,12 +93,6 @@ export default // ex: 'ws-jobs-' str = `ws-${data.group_name}-${data.job}`; } - else if(data.group_name==="workflow_events"){ - // The naming scheme is "ws" then a - // dash (-) and the group_name, then the job ID - // ex: 'ws-jobs-' - str = `ws-${data.group_name}-${data.workflow_job_id}`; - } else if(data.group_name==="ad_hoc_command_events"){ // The naming scheme is "ws" then a // dash (-) and the group_name, then the job ID diff --git a/awx/ui/client/src/standard-out/standard-out.controller.js b/awx/ui/client/src/standard-out/standard-out.controller.js index ffed821d7d..2edfa68abd 100644 --- a/awx/ui/client/src/standard-out/standard-out.controller.js +++ b/awx/ui/client/src/standard-out/standard-out.controller.js @@ -58,6 +58,10 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, $scope.credential_name = (data.summary_fields.credential) ? data.summary_fields.credential.name : ''; $scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : ''; $scope.cloud_credential_url = (data.cloud_credential) ? '/#/credentials/' + data.cloud_credential : ''; + if(data.summary_fields && data.summary_fields.source_workflow_job && + data.summary_fields.source_workflow_job.id){ + $scope.workflow_result_link = `/#/workflows/${data.summary_fields.source_workflow_job.id}`; + } $scope.playbook = data.playbook; $scope.credential = data.credential; $scope.cloud_credential = data.cloud_credential; diff --git a/awx/ui/client/src/workflow-results/workflow-results.controller.js b/awx/ui/client/src/workflow-results/workflow-results.controller.js index d80385141c..af94f001c3 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.controller.js +++ b/awx/ui/client/src/workflow-results/workflow-results.controller.js @@ -7,6 +7,8 @@ export default ['workflowData', 'ParseTypeChange', 'ParseVariableString', 'WorkflowService', + 'count', + '$state', function(workflowData, workflowResultsService, workflowDataOptions, @@ -15,7 +17,9 @@ export default ['workflowData', $scope, ParseTypeChange, ParseVariableString, - WorkflowService + WorkflowService, + count, + $state ) { var getTowerLinks = function() { @@ -57,6 +61,7 @@ export default ['workflowData', $scope.workflow_nodes = workflowNodes; $scope.workflowOptions = workflowDataOptions.actions.GET; $scope.labels = jobLabels; + $scope.count = count.val; // turn related api browser routes into tower routes getTowerLinks(); @@ -113,22 +118,38 @@ export default ['workflowData', init(); - $scope.$on(`ws-workflow_events-${$scope.workflow.id}`, function(e, data) { - - WorkflowService.updateStatusOfNode({ - treeData: $scope.treeData, - nodeId: data.workflow_node_id, - status: data.status, - unified_job_id: data.unified_job_id - }); - - $scope.$broadcast("refreshWorkflowChart"); - }); - // Processing of job-status messages from the websocket $scope.$on(`ws-jobs`, function(e, data) { + // Update the workflow job's unified job: if (parseInt(data.unified_job_id, 10) === parseInt($scope.workflow.id,10)) { - $scope.workflow.status = data.status; + $scope.workflow.status = data.status; + + if(data.status === "successful" || data.status === "failed"){ + $state.go('.', null, { reload: true }); + } + } + // Update the jobs spawned by the workflow: + if(data.hasOwnProperty('workflow_job_id') && + parseInt(data.workflow_job_id, 10) === parseInt($scope.workflow.id,10)){ + + WorkflowService.updateStatusOfNode({ + treeData: $scope.treeData, + nodeId: data.workflow_node_id, + status: data.status, + unified_job_id: data.unified_job_id + }); + + $scope.workflow_nodes.forEach(node => { + if(parseInt(node.id) === parseInt(data.workflow_node_id)){ + node.summary_fields.job = { + status: data.status + }; + } + }); + + $scope.count = workflowResultsService + .getCounts($scope.workflow_nodes); + $scope.$broadcast("refreshWorkflowChart"); } }); }]; diff --git a/awx/ui/client/src/workflow-results/workflow-results.route.js b/awx/ui/client/src/workflow-results/workflow-results.route.js index 70b54c940d..904584ec25 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.route.js +++ b/awx/ui/client/src/workflow-results/workflow-results.route.js @@ -18,8 +18,7 @@ export default { data: { socket: { "groups":{ - "jobs": ["status_changed"], - "workflow_events": [] + "jobs": ["status_changed"] } } }, @@ -61,6 +60,18 @@ export default { }); return defer.promise; }], + // after the GET for the workflow & it's nodes, this helps us keep the + // status bar from flashing as rest data comes in. If the workflow + // is finished and there's a playbook_on_stats event, go ahead and + // resolve the count so you don't get that flashing! + count: ['workflowData', 'workflowNodes', 'workflowResultsService', 'Rest', '$q', function(workflowData, workflowNodes, workflowResultsService, Rest, $q) { + var defer = $q.defer(); + defer.resolve({ + val: workflowResultsService + .getCounts(workflowNodes), + countFinished: true}); + return defer.promise; + }], // GET for the particular jobs labels to be displayed in the // left-hand pane jobLabels: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) { diff --git a/awx/ui/client/src/workflow-results/workflow-results.service.js b/awx/ui/client/src/workflow-results/workflow-results.service.js index 08d38378d8..2d3fddf2f4 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.service.js +++ b/awx/ui/client/src/workflow-results/workflow-results.service.js @@ -7,6 +7,31 @@ export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', 'InitiatePlaybookRun', function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors, InitiatePlaybookRun) { var val = { + getCounts: function(workflowNodes){ + var nodeArr = []; + workflowNodes.forEach(node => { + if(node && node.summary_fields && node.summary_fields.job && node.summary_fields.job.status){ + nodeArr.push(node.summary_fields.job.status); + } + }); + // use the workflow nodes data populate above to get the count + var count = { + successful : _.filter(nodeArr, function(o){ + return o === "successful"; + }), + failed : _.filter(nodeArr, function(o){ + return o === "failed" || o === "error" || o === "canceled"; + }) + }; + + // turn the count into an actual count, rather than a list of + // statuses + Object.keys(count).forEach(key => { + count[key] = count[key].length; + }); + + return count; + }, deleteJob: function(workflow) { Prompt({ hdr: 'Delete Job', diff --git a/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.block.less b/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.block.less index 38e57d4883..3f9bf3d8f0 100644 --- a/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.block.less +++ b/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.block.less @@ -5,42 +5,31 @@ flex: 0 0 auto; width: 100%; margin-top: 10px; + margin-bottom: 15px; } -.WorkflowStatusBar-ok, -.WorkflowStatusBar-changed, -.WorkflowStatusBar-unreachable, -.WorkflowStatusBar-failures, -.WorkflowStatusBar-skipped, +.WorkflowStatusBar-successful, +.WorkflowStatusBar-failed, +.WorkflowStatusBar-pending, .WorkflowStatusBar-noData { height: 15px; border-top: 5px solid @default-bg; border-bottom: 5px solid @default-bg; } -.WorkflowStatusBar-ok { +.WorkflowStatusBar-successful { background-color: @default-succ; display: flex; flex: 0 0 auto; } -.WorkflowStatusBar-changed { - background-color: @default-warning; - flex: 0 0 auto; -} - -.WorkflowStatusBar-unreachable { - background-color: @default-unreachable; - flex: 0 0 auto; -} - -.WorkflowStatusBar-failures { +.WorkflowStatusBar-failed { background-color: @default-err; flex: 0 0 auto; } -.WorkflowStatusBar-skipped { - background-color: @default-link; +.WorkflowStatusBar-pending { + background-color: @b7grey; flex: 0 0 auto; } @@ -58,23 +47,14 @@ border-radius: 5px; } -.WorkflowStatusBar-tooltipBadge--ok { +.WorkflowStatusBar-tooltipBadge--successful { background-color: @default-succ; } -.WorkflowStatusBar-tooltipBadge--unreachable { - background-color: @default-unreachable; -} - -.WorkflowStatusBar-tooltipBadge--skipped { - background-color: @default-link; -} - -.WorkflowStatusBar-tooltipBadge--changed { - background-color: @default-warning; -} - -.WorkflowStatusBar-tooltipBadge--failures { +.WorkflowStatusBar-tooltipBadge--failed { background-color: @default-err; - +} + +.WorkflowStatusBar-tooltipBadge--pending { + background-color: @b7grey; } diff --git a/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.directive.js b/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.directive.js index a6899eb0da..c53fc2dcba 100644 --- a/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.directive.js +++ b/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.directive.js @@ -4,27 +4,25 @@ * All Rights Reserved *************************************************/ -// import WorkflowStatusBarController from './host-status-bar.controller'; export default [ 'templateUrl', function(templateUrl) { return { scope: true, templateUrl: templateUrl('workflow-results/workflow-status-bar/workflow-status-bar'), restrict: 'E', - // controller: standardOutLogController, link: function(scope) { - // as count is changed by event data coming in, - // update the host status bar + // as count is changed by jobs coming in, + // update the workflow status bar scope.$watch('count', function(val) { if (val) { Object.keys(val).forEach(key => { - // reposition the hosts status bar by setting + // reposition the workflow status bar by setting // the various flex values to the count of - // those hosts + // those jobs $(`.WorkflowStatusBar-${key}`) .css('flex', `${val[key]} 0 auto`); - // set the tooltip to give how many hosts of + // set the tooltip to give how many jobs of // each type if (val[key] > 0) { scope[`${key}CountTip`] = `${key}${val[key]}`; diff --git a/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.partial.html b/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.partial.html index e0efddc7b6..c2bc7d87a5 100644 --- a/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-status-bar/workflow-status-bar.partial.html @@ -1,26 +1,14 @@
-
-
+
-
-
-
+ aw-tool-tip="{{failedCountTip}}" + data-tip-watch="failedCountTip">
From c30ac365c3442248ccc6d5a58210afa12bd3d23a Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Thu, 8 Dec 2016 15:21:52 -0500 Subject: [PATCH 061/595] Fix broken column-sort toggles, add unit test coverage for column-sort toggles, #4268 (#4281) --- .../column-sort/column-sort.controller.js | 19 ++-- .../column-sort/column-sort.partial.html | 2 +- .../smart-search/smart-search.controller.js | 2 +- .../column-sort/column-sort.directive-test.js | 101 ++++++++++++++++++ 4 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 awx/ui/tests/spec/column-sort/column-sort.directive-test.js diff --git a/awx/ui/client/src/shared/column-sort/column-sort.controller.js b/awx/ui/client/src/shared/column-sort/column-sort.controller.js index e5ce39ff3c..2dee59818c 100644 --- a/awx/ui/client/src/shared/column-sort/column-sort.controller.js +++ b/awx/ui/client/src/shared/column-sort/column-sort.controller.js @@ -1,9 +1,7 @@ export default ['$scope', '$state', 'QuerySet', 'GetBasePath', function($scope, $state, qs, GetBasePath) { - let queryset, path, - order_by = $state.params[`${$scope.columnIterator}_search`].order_by, - activeField = isDescending(order_by) ? order_by.substring(1, order_by.length) : order_by; + let queryset, path, order_by; function isDescending(str) { if (str){ @@ -15,15 +13,16 @@ export default ['$scope', '$state', 'QuerySet', 'GetBasePath', } } function invertOrderBy(str) { - return order_by.charAt(0) === '-' ? `${str.substring(1, str.length)}` : `-${str}`; + return str.charAt(0) === '-' ? `${str.substring(1, str.length)}` : `-${str}`; } $scope.orderByIcon = function() { + order_by = $state.params[`${$scope.columnIterator}_search`].order_by; // column sort is inactive - if (activeField !== $scope.columnField) { + if (order_by !== $scope.columnField && order_by !== invertOrderBy($scope.columnField)) { return 'fa-sort'; } // column sort is active (governed by order_by) and descending - else if (isDescending(order_by)) { + else if (isDescending($state.params[`${$scope.columnIterator}_search`].order_by)) { return 'fa-sort-down'; } // column sort is active governed by order_by) and asscending @@ -33,17 +32,19 @@ export default ['$scope', '$state', 'QuerySet', 'GetBasePath', }; $scope.toggleColumnOrderBy = function() { - // toggle active sort order - if (activeField === $scope.columnField) { + let order_by = $state.params[`${$scope.columnIterator}_search`].order_by; + + if (order_by === $scope.columnField || order_by === invertOrderBy($scope.columnField)) { order_by = invertOrderBy(order_by); } // set new active sort order else { order_by = $scope.columnField; } + queryset = _.merge($state.params[`${$scope.columnIterator}_search`], { order_by: order_by }); path = GetBasePath($scope.basePath) || $scope.basePath; - $state.go('.', { [$scope.iterator + '_search']: queryset }); + $state.go('.', { [$scope.columnIterator + '_search']: queryset }); qs.search(path, queryset).then((res) =>{ $scope.dataset = res.data; $scope.collection = res.data.results; diff --git a/awx/ui/client/src/shared/column-sort/column-sort.partial.html b/awx/ui/client/src/shared/column-sort/column-sort.partial.html index 96ea648b6c..63d45e69c2 100644 --- a/awx/ui/client/src/shared/column-sort/column-sort.partial.html +++ b/awx/ui/client/src/shared/column-sort/column-sort.partial.html @@ -1,4 +1,4 @@ {{columnLabel}} - + diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index 4fa3517547..87dc33198a 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -24,7 +24,7 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' function stripDefaultParams(params) { let stripped =_.pick(params, (value, key) => { // setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value - return defaults[key] !== value && key !== 'page' && key !== 'page_size' && defaults[key] !== null; + return defaults[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaults[key] !== null; }); return _(stripped).map(qs.decodeParam).flatten().value(); } diff --git a/awx/ui/tests/spec/column-sort/column-sort.directive-test.js b/awx/ui/tests/spec/column-sort/column-sort.directive-test.js new file mode 100644 index 0000000000..4cb1a3ec53 --- /dev/null +++ b/awx/ui/tests/spec/column-sort/column-sort.directive-test.js @@ -0,0 +1,101 @@ +'use strict'; + +describe('Directive: column-sort', () =>{ + + let $scope, template, $compile, QuerySet, GetBasePath; + + beforeEach(angular.mock.module('templateUrl')); + beforeEach(function(){ + + this.mock = { + dataset: [ + {name: 'zero', idx: 0}, + {name: 'one', idx: 1}, + {name: 'two', idx: 2} + ] + }; + + this.name_field = angular.element(` + `); + + this.idx_field = angular.element(` + `); + + this.$state = { + params: {}, + go: jasmine.createSpy('go') + }; + + + angular.mock.module('ColumnSortModule', ($provide) =>{ + + + QuerySet = jasmine.createSpyObj('qs', ['search']); + QuerySet.search.and.callFake(() => { return { then: function(){} } }); + GetBasePath = jasmine.createSpy('GetBasePath'); + $provide.value('QuerySet', QuerySet); + $provide.value('GetBasePath', GetBasePath); + $provide.value('$state', this.$state); + + }); + }); + + beforeEach(angular.mock.inject(($templateCache, _$rootScope_, _$compile_) => { + template = window.__html__['client/src/shared/column-sort/column-sort.partial.html']; + $templateCache.put('/static/partials/shared/column-sort/column-sort.partial.html', template); + + $compile = _$compile_; + $scope = _$rootScope_.$new(); + })); + + it('should be ordered by name', function(){ + + this.$state.params = { + mock_search: {order_by: 'name'} + }; + + $compile(this.name_field)($scope); + $compile(this.idx_field)($scope) + + $scope.$digest(); + expect( $(this.name_field).find('.columnSortIcon').hasClass('fa-sort-up') ).toEqual(true); + expect( $(this.idx_field).find('.columnSortIcon').hasClass('fa-sort') ).toEqual(true); + }); + + it('should toggle to ascending name order, then ascending idx, then descending idx', function(){ + + this.$state.params = { + mock_search: {order_by: 'idx'} + }; + + $compile(this.name_field)($scope); + $compile(this.idx_field)($scope) + + $scope.$digest(); + + $(this.name_field).click(); + expect( $(this.name_field).find('.columnSortIcon').hasClass('fa-sort-up') ).toEqual(true); + expect( $(this.idx_field).find('.columnSortIcon').hasClass('fa-sort') ).toEqual(true); + + $(this.idx_field).click(); + expect( $(this.name_field).find('.columnSortIcon').hasClass('fa-sort') ).toEqual(true); + expect( $(this.idx_field).find('.columnSortIcon').hasClass('fa-sort-up') ).toEqual(true); + + $(this.idx_field).click(); + expect( $(this.name_field).find('.columnSortIcon').hasClass('fa-sort') ).toEqual(true); + expect( $(this.idx_field).find('.columnSortIcon').hasClass('fa-sort-down') ).toEqual(true) + }); + +}); \ No newline at end of file From e5278e2291bb0db8b1b911dc298a5a2e125d049c Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Thu, 8 Dec 2016 16:40:07 -0500 Subject: [PATCH 062/595] Fix wfj node related destination. --- awx/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index f15e762c03..b5a5b326ef 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2420,7 +2420,7 @@ class WorkflowJobNodeSerializer(WorkflowNodeBaseSerializer): res['failure_nodes'] = reverse('api:workflow_job_node_failure_nodes_list', args=(obj.pk,)) res['always_nodes'] = reverse('api:workflow_job_node_always_nodes_list', args=(obj.pk,)) if obj.job: - res['job'] = reverse('api:job_detail', args=(obj.job.pk,)) + res['job'] = obj.job.get_absolute_url() if obj.workflow_job: res['workflow_job'] = reverse('api:workflow_job_detail', args=(obj.workflow_job.pk,)) return res From 44e83dbcd70c71b14dfbe3228816c8582a3191fc Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Fri, 9 Dec 2016 09:37:16 -0500 Subject: [PATCH 063/595] Disabling fields for auditors in CTinT --- .../configuration-auth.controller.js | 3 +++ .../configuration/configuration.controller.js | 6 ++++++ .../configuration-jobs.controller.js | 7 +++++-- .../configuration-system.controller.js | 20 +++++-------------- .../ui-form/configuration-ui.controller.js | 5 ++++- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js index beb295fc98..a060141620 100644 --- a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js @@ -6,6 +6,7 @@ export default [ '$scope', + '$rootScope', '$state', '$stateParams', '$timeout', @@ -25,6 +26,7 @@ export default [ 'ParseTypeChange', function( $scope, + $rootScope, $state, $stateParams, $timeout, @@ -167,6 +169,7 @@ export default [ placeholder: ConfigurationUtils.formatPlaceholder($scope.$parent.configDataResolve[key].placeholder, key) || null, dataTitle: $scope.$parent.configDataResolve[key].label, required: $scope.$parent.configDataResolve[key].required, + ngDisabled: $rootScope.user_is_system_auditor }); } diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js index b769ffc8c4..f4ee38e236 100644 --- a/awx/ui/client/src/configuration/configuration.controller.js +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -362,6 +362,12 @@ export default [ $scope.toggleForm = function(key) { + if($rootScope.user_is_system_auditor) { + // Block system auditors from making changes + event.preventDefault(); + return; + } + $scope[key] = !$scope[key]; Wait('start'); var payload = {}; diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js index 50cecf4832..1ea93ad691 100644 --- a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js @@ -6,6 +6,7 @@ export default [ '$scope', + '$rootScope', '$state', '$timeout', 'ConfigurationJobsForm', @@ -15,6 +16,7 @@ export default [ 'GenerateForm', function( $scope, + $rootScope, $state, $timeout, ConfigurationJobsForm, @@ -34,7 +36,7 @@ export default [ value: command }); }); - + // Disable the save button for non-superusers form.buttons.save.disabled = 'vm.updateProhibited'; @@ -51,7 +53,8 @@ export default [ toggleSource: key, dataPlacement: 'top', dataTitle: $scope.$parent.configDataResolve[key].label, - required: $scope.$parent.configDataResolve[key].required + required: $scope.$parent.configDataResolve[key].required, + ngDisabled: $rootScope.user_is_system_auditor }); } diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js index 90206c9d67..3751e298a5 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js +++ b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js @@ -5,9 +5,10 @@ *************************************************/ export default [ - '$scope', '$state', 'AngularCodeMirror', 'ConfigurationSystemForm', 'ConfigurationService', 'ConfigurationUtils', 'GenerateForm', + '$rootScope', '$scope', '$state', 'AngularCodeMirror', 'Authorization', 'ConfigurationSystemForm', 'ConfigurationService', + 'ConfigurationUtils', 'GenerateForm', function( - $scope, $state, AngularCodeMirror, ConfigurationSystemForm, ConfigurationService, ConfigurationUtils, GenerateForm + $rootScope, $scope, $state, AngularCodeMirror, Authorization, ConfigurationSystemForm, ConfigurationService, ConfigurationUtils, GenerateForm ) { var systemVm = this; var generator = GenerateForm; @@ -29,7 +30,8 @@ export default [ toggleSource: key, dataPlacement: 'top', dataTitle: $scope.$parent.configDataResolve[key].label, - required: $scope.$parent.configDataResolve[key].required + required: $scope.$parent.configDataResolve[key].required, + ngDisabled: $rootScope.user_is_system_auditor }); } @@ -40,18 +42,6 @@ export default [ related: true }); - - $scope.$on('populated', function() { - // $scope.$parent.parseType = 'json'; - // ParseTypeChange({ - // scope: $scope.$parent, - // variable: 'LICENSE', - // parse_variable: 'parseType', - // field_id: 'configuration_system_template_LICENSE', - // readOnly: true - // }); - }); - angular.extend(systemVm, { }); diff --git a/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js b/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js index c807b4807a..df5dab907d 100644 --- a/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js +++ b/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js @@ -6,6 +6,7 @@ export default [ '$scope', + '$rootScope', '$state', '$timeout', 'ConfigurationUiForm', @@ -14,6 +15,7 @@ 'GenerateForm', function( $scope, + $rootScope, $state, $timeout, ConfigurationUiForm, @@ -54,7 +56,8 @@ toggleSource: key, dataPlacement: 'top', dataTitle: $scope.$parent.configDataResolve[key].label, - required: $scope.$parent.configDataResolve[key].required + required: $scope.$parent.configDataResolve[key].required, + ngDisabled: $rootScope.user_is_system_auditor }); } From aacae7b8441af7ef431ab1aeb7dc5ef3c975833b Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Fri, 9 Dec 2016 12:33:01 -0500 Subject: [PATCH 064/595] ui build system documents (#4353) * add ui build system documentation, pin nodejs + npm engines to versions vetted for Tower * Update ui_build_system.md * Update ui_build_system.md --- awx/ui/package.json | 2 +- docs/ui_build_system.md | 237 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 docs/ui_build_system.md diff --git a/awx/ui/package.json b/awx/ui/package.json index 9c876a2db3..4decc6fc29 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -13,7 +13,7 @@ }, "engines": { "node": "^6.3.1", - "npm": "3.10.7" + "npm": "^3.10.3" }, "scripts": { "build-docker-machine": "ip=$(docker-machine ip $DOCKER_MACHINE_NAME); npm set ansible-tower:django_host ${ip}; grunt dev;", diff --git a/docs/ui_build_system.md b/docs/ui_build_system.md new file mode 100644 index 0000000000..49238df093 --- /dev/null +++ b/docs/ui_build_system.md @@ -0,0 +1,237 @@ +# UI BUILD SYSTEM + +### Table of Contents + +1. [Care and Keeping of NodeJS + NPM](#nodejs-and-npm) + 1. [Pin NodeJS & NPM versions](#pin-nodejs-npm-versions) + 2. [Use NVM to manage multple Node/NPM installations](#use-nvm) + 3.. [Add, incremenet, remove a package](#add-upgrade-remove-npm-package) + 4. [dependency, devDependency, or optionalDependency?](#npm-dependency-types) + +2. [Webpack](#webpack) + 1. [What does Webpack handle?](#webpack-what-do) + 2. [Add / remove a vendor module](#add-upgrade-remove-vendor-module) + 3. [Strategies: loading a module](#loading-modules) + 4. [Strategies: exposing a module to application code](#exposing-modules) + + +3. [Grunt](#grunt) + 1. [What does Grunt handle?](#grunt-what-do) + 2. [Add / remove a Grunt task](#add-remove-upgrade-grunt-task) + 3. [Task concurrency & process lifecycles](#grunt-task-concurrency) + +4. [Karma](#karma) + 1. [Developer configuration](#karma-developer-config) + 2. [CI configuration](#karma-ci-config) + +5. [Interfaces & Environments](#interfaces-and-environments) + 1. [NPM script targets](#npm-scripts) + 2. [Makefile targets](#make-targets) + 3. [NPM config variables](#npm-config-variables) + 4. [Other environment variables](#environment-variables) + +6. References / Resources + + +# Care and Keeping of NodeJS + NPM +## Pin NodeJS & NPM versions + +NodeJS began packaging their releases with a bundled version of NPM. Occasionally, the version of NPM that ships with a given NodeJS release can be unstable. For example, the v6 LTS of Node shipped with a version of NPM that introduced a regression that broke module installation for any package with platform-specific dependencies. + +For this reason, it's strongly advised to pin development environments, CI, and release pipelines to vetted releases of NodeJS + NPM. + +Pinned versions are best expressed through the engine field in `package.json`. + +```json + "engines": { + "node": "^6.3.1", + "npm": "=3.10.3" + } + ``` + +## Use NVM to manage multiple NodeJS + NPM installations + +A system installation of Node raises *many* challenges on a development or shared system: user permissions, shared (global) modules, and installation paths for multiple versions. `nvm` is a light executable that addresses all of these issues. In the context of Tower, we use nvm to quickly switch between versions of NodeJS + NPM. + +Version support per Tower release +3.0.* - NodeJS v0.12.17 & NPM v2.15.1 +3.1.* - NodeJS 6.3.1 * & NPM 3.10.3 + +* [nvm installation guide](https://github.com/creationix/nvm#installation) +* [additional shell integrations](https://github.com/creationix/nvm#deeper-shell-integration) + +```bash +$ nvm install 6.3 +$ nvm use 6.3 + +``` + +## Adding, incrementing, removing packages via NPM + +The Tower package utilizes an `npm-shrinkwrap.json` file to freeze dependency resolution. When `npm install` runs, it will prefer to resolve dependencies as frozen in the shrinkwrap file before it ingests versions in `package.json`. To update the shrinkwrap file with a new dependency, pass the `--save` argument e.g. `npm install fooify@1.2.3 --save`. + +*Do not run `npm shrinkwrap` when add/updating dependencies*. This will re-generate the entire conflict resolution tree, which will churn over time. Instead, depend on `--save`, `--save-dev` and `--save-optional` to create/update the shrinkwrapped package. + +## What's a dependency, devDependency, or optionalDependency + +`dependency` - Bundled in the Tower product. Customer-facing. +`devDependency` - Used in the development, build, and release pipelines +`optionalDependency` - Platform-specific dependencies, or dependencies which should not cause `npm install` to exit with an error if these modules fail to install. + +# Webpack + +## What does Webpack handle? + +Webpack ingests our vendor and application modules, and outputs bundled code optimized for development or production. Configuration lives in `webpack.config.js`. + +Webpack is a highly pluggable framework ([list of vetted plugins](https://webpack.github.io/docs/list-of-plugins.html)) We make use of the following plugins: + +* [ProvidePlugin](https://webpack.github.io/docs/list-of-plugins.html#provideplugin) - automatically loads and exports a module into specified namespace. A modular approach to loading modules that you would otherwise have to load into global namespace. Example: + +```javascript +// webpack.config.js +plugins: { + new webpack.ProvidePlugin({ + '$': 'jquery', + }) +} +``` + +```javascript +// some-application-module.js +// the following code: +$('.my-thingy').click(); + +// is transformed into: +var $ = require('jquery'); +$('.my-thingy').click(); +``` + +* [CommonChunksPlugin](https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin) - a [chunk](https://webpack.github.io/docs/code-splitting.html#chunk-content) is Webpack's unit of code-splitting. This plugin' chunk consolidation strategy helps us split our bundled vendor code from our bundled application code, which *dramatically reduces* rebuild and browser loading time in development. + +Currently, Tower is split into two output bundles: `tower.vendor.js` and `tower.js`. This would be the plugin configuration to update to additionally split application code into more portable components (example: for usage in the Insights UI). + +* [DefinePlugin](https://webpack.github.io/docs/list-of-plugins.html#defineplugin) - injects a module that behaves like a global constant, which can be defined/configured as compile time. Tower uses this plugin to allow command-line arguments passed in at build time to be consumed by application code. + +* [UglifyJSPlugin](https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin) (production-only) - removes whitespace and minifies output. The mangle option can be used for an addiction layer of obfuscation, but it can also cause namespace issues. + +## Add / remove a vendor module + +First, [install the package via npm](#add-upgrade-remove-npm-package). If the package doesn't export its contents as a CommonJS, AMD, or SystemJS module you will need to [write a module loader](https://webpack.github.io/docs/how-to-write-a-loader.html). + +Not all packages correctly import their own dependencies. Some packages (notable: most jquery plugins) assume a certain dependency will already be in the global namespace. You will need to shim dependencies into these modules using Webpack's [export loader](https://webpack.github.io/docs/shimming-modules.html#exporting). + +To bundle a new dependency in `tower.vendor.js`, add it to the `vendorPkgs` array in `webpack.config.js`. + +## Strategies: loading a module + +Webpack ingests code through a concept called a `loader`. [What is a loader?, exactly?](http://webpack.github.io/docs/using-loaders.html#what-are-loaders) + +Loaders can be chained together to perform a complex series of transformations and exports, or used in isolation to target a specific module. + +The Webpack loaders used by Tower: + +[Babel loader](https://github.com/babel/babel-loader) - loads files matching a pattern +[imports loader](https://github.com/webpack/imports-loader) - shims dependency namespace, or constrains our own module loading strategies for this package (e.g. prefer to use CJS because AMD strategy is broken in package) + + + +# Grunt + +[Grunt](http://gruntjs.com/) is a modular task runner. Functionally, it serves the same purpose as a Makefile or set of bash scripts. Grunt helps normalize the interfaces between disparate pieces of tooling. For purposes of Tower, Grunt also simplifies managing the lifecycle of concurrent child processes. + +## What does Grunt handle?a> + +Grunt cleans up build artifacts, copies source files, lints, runs 18n text extraction & compilation, and transforms LESS to CSS. + +Other development-only Grunt tasks will poll for file changes, run tasks when a subset of files changes, and raise an instance of BrowserSync (reloads browser on built changes) proxied to an instance of the Django API, running in a native Docker container or container inside of a Docker Machine. + +Grunt internally uses minimatch [file globbing patterns](http://gruntjs.com/configuring-tasks#globbing-patterns) + +## Add / change / remove a Grunt task + +Grunt tasks live in `awx/ui/grunt-tasks/` and are organized in a file-per-plugin pattern. + +The plugin `load-grunt-configs` will automatically load and register tasks read from the configuration files in `awx/ui/grunt-tasks`. This reduces the amount of boilerplate required to write, load, register, each task configuration. + + +FEach task may be configured with a set of default option, plus additional targets that inherit or override defaults. For example, all tasks in `grunt-tasks/clean.js` run with `-f` or `--force` flag. `grunt-contrib-clean` + +```javascript +module.exports = { + options: { force: true }, + static: 'static/*', + coverage: 'coverage/*', + tmp: '../../tmp', + jshint: 'coverage/jshint.xml' +}; +``` + +## Grunt task concurrency + +By default, Grunt tasks are run in a series. [grunt-concurrent] is a plugin that allows us to parallelize certain tasks, to speed up the overall build process. + +Note: certain polling tasks *must always be run on a thread that remains alive*. For example, the `webpack` tasks interact with Webpack's API. Therefor when Webpack's `livereload` option is on, Grunt `webpack` tasks should be spawned in series prior to Grunt `watch` tasks. + +# Karma + +`karma.conf.js` is a generic configuration shared between developer and CI unit test runs. + +The base configuration is suitable for live development. Additional command-line arguments supplement this general config to suit CI systems. + +An [npm script](#npm-scripts) interface is provided to run either of these configurations: `npm test` (base) `npm test:ci` + +# Interfaces & Environments + +The UI build system is intended for use through [NPM scripts](https://docs.npmjs.com/misc/scripts). NPM scripts are preferable to just running `grunt sometask` because `npm run` will look for `node_modules/.bin` executables, allowing us to manage CI/release executable versions through `package.json`. You would otherwise have to append these to the executor's $PATH. + +`npm run` targets are run in a shell, which makes them a flexible way of mixing together Python, Ruby, Node, or other CLI tooling in a project. + +## Tower's NPM script targets + +Below is a reference of what each script target does in human language, and then what you can expect the script to execute. + +------- + +Builds a development version of the UI with a BrowserSync instance proxied to a Docker Machine +```bash +$ DOCKER_MACHINE_NAME=default npm run build-docker-machine +$ ip=$(docker-machine ip $DOCKER_MACHINE_NAME); npm set ansible-tower:django_host ${ip}; grunt dev; +``` + +Builds a development version of the UI with a BrowserSync instance proxied to a native Docker container +```bash +$ DOCKER_CID=1a2b3c4d5e npm run build-docker +$ ip=`docker inspect --format '{{ .NetworkSettings.IPAddress }}' $DOCkER_CID` | npm set config ansible-tower:django_host ${ip}; grunt dev; +``` + +Builds a development version of the UI. No filesystem polling. Re-run after each new revision. +```bash +$ npm run build-devel +``` + +Builds a production version of the UI. +```bash +$ npm run build-release +``` + +Launches unit test runners in assorted browsers. Alias for `npm run test` +```bash +$ npm test +``` + +Launches unit test runners headless in PhantomJS. Alias for `npm run test:ci` +```bash +$ npm test:ci +``` + +Extracts i18n string markers to a .pot template. +```bash +$ npm run pot +``` + +Builds i18n language files with regard to .pot. +```bash +$ npm run languages +``` + From 514839938bacb9eda7dd45f8d30a9663f0e5edd3 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 9 Dec 2016 13:27:58 -0500 Subject: [PATCH 065/595] fix two inv updates created from 1 jt run --- awx/main/scheduler/__init__.py | 3 ++ awx/main/scheduler/dependency_graph.py | 10 +++---- awx/main/scheduler/partial.py | 1 + .../test_scheduler_inventory_update.py | 29 +++++++++++++++++-- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/awx/main/scheduler/__init__.py b/awx/main/scheduler/__init__.py index 8569fb5cfc..5221f340a6 100644 --- a/awx/main/scheduler/__init__.py +++ b/awx/main/scheduler/__init__.py @@ -240,6 +240,9 @@ class TaskManager(): dependencies.append(project_task) # Inventory created 2 seconds behind job + ''' + Inventory may have already been synced from a provision callback. + ''' inventory_sources_already_updated = task.get_inventory_sources_already_updated() for inventory_source_task in self.graph.get_inventory_sources(task['inventory_id']): diff --git a/awx/main/scheduler/dependency_graph.py b/awx/main/scheduler/dependency_graph.py index 7b66a8fe1b..846a194b27 100644 --- a/awx/main/scheduler/dependency_graph.py +++ b/awx/main/scheduler/dependency_graph.py @@ -117,10 +117,6 @@ class DependencyGraph(object): if not latest_inventory_update: return True - # TODO: Other finished, failed cases? i.e. error ? - if latest_inventory_update['status'] in ['failed', 'canceled']: - return True - ''' This is a bit of fuzzy logic. If the latest inventory update has a created time == job_created_time-2 @@ -138,7 +134,11 @@ class DependencyGraph(object): timeout_seconds = timedelta(seconds=latest_inventory_update['inventory_source__update_cache_timeout']) if (latest_inventory_update['finished'] + timeout_seconds) < now: return True - + + if latest_inventory_update['inventory_source__update_on_launch'] is True and \ + latest_inventory_update['status'] in ['failed', 'canceled', 'error']: + return True + return False def mark_system_job(self): diff --git a/awx/main/scheduler/partial.py b/awx/main/scheduler/partial.py index d16634f369..18c5c63d46 100644 --- a/awx/main/scheduler/partial.py +++ b/awx/main/scheduler/partial.py @@ -151,6 +151,7 @@ class InventoryUpdateLatestDict(InventoryUpdateDict): FIELDS = ( 'id', 'status', 'created', 'celery_task_id', 'inventory_source_id', 'finished', 'inventory_source__update_cache_timeout', 'launch_type', + 'inventory_source__update_on_launch', ) model = InventoryUpdate diff --git a/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py b/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py index 5e49eec729..eeace8243d 100644 --- a/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py +++ b/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py @@ -26,6 +26,22 @@ def successful_inventory_update_latest_cache_expired(inventory_update_latest_fac return iu +@pytest.fixture +def failed_inventory_update_latest_cache_zero(failed_inventory_update_latest): + iu = failed_inventory_update_latest + iu['inventory_source__update_cache_timeout'] = 0 + iu['inventory_source__update_on_launch'] = True + iu['finished'] = iu['created'] + timedelta(seconds=2) + iu['status'] = 'failed' + return iu + + +@pytest.fixture +def failed_inventory_update_latest_cache_non_zero(failed_inventory_update_latest_cache_zero): + failed_inventory_update_latest_cache_zero['inventory_source__update_cache_timeout'] = 10000000 + return failed_inventory_update_latest_cache_zero + + class TestStartInventoryUpdate(): def test_pending(self, scheduler_factory, pending_inventory_update): scheduler = scheduler_factory(tasks=[pending_inventory_update]) @@ -79,9 +95,18 @@ class TestCreateDependentInventoryUpdate(): scheduler.start_task.assert_called_with(waiting_inventory_update, [pending_job]) - def test_last_update_failed(self, scheduler_factory, pending_job, failed_inventory_update, failed_inventory_update_latest, waiting_inventory_update, inventory_id_sources): + def test_last_update_timeout_zero_failed(self, scheduler_factory, pending_job, failed_inventory_update, failed_inventory_update_latest_cache_zero, waiting_inventory_update, inventory_id_sources): scheduler = scheduler_factory(tasks=[failed_inventory_update, pending_job], - latest_inventory_updates=[failed_inventory_update_latest], + latest_inventory_updates=[failed_inventory_update_latest_cache_zero], + create_inventory_update=waiting_inventory_update, + inventory_sources=inventory_id_sources) + scheduler._schedule() + + scheduler.start_task.assert_called_with(waiting_inventory_update, [pending_job]) + + def test_last_update_timeout_non_zero_failed(self, scheduler_factory, pending_job, failed_inventory_update, failed_inventory_update_latest_cache_non_zero, waiting_inventory_update, inventory_id_sources): + scheduler = scheduler_factory(tasks=[failed_inventory_update, pending_job], + latest_inventory_updates=[failed_inventory_update_latest_cache_non_zero], create_inventory_update=waiting_inventory_update, inventory_sources=inventory_id_sources) scheduler._schedule() From cc7c2957cffdfe38e8c5a3970d1db3acd8e1ae3e Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 5 Dec 2016 15:30:21 -0500 Subject: [PATCH 066/595] always chain failures * When inv and proj updates trigger from a JT run, if either update fails then the job template should get marked failed. Before this commit, the job template would get marked failed ONLY if there was enough capacity to run all the associated updates within the same schedule() call. If, instead, the associated updates were ran in another schedule() call, the failure chain was lost. This changeset fixes that by saving the necessary data in the dependent_jobs relationship so that the failure is always chained. --- awx/main/models/unified_jobs.py | 7 ++++ awx/main/scheduler/__init__.py | 38 ++++++++++++++++++ awx/main/scheduler/partial.py | 39 +++++++++++++++++-- awx/main/tests/unit/scheduler/conftest.py | 1 + .../test_scheduler_inventory_update.py | 18 +++++++++ 5 files changed, 100 insertions(+), 3 deletions(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 31afe69d32..a9e1e631df 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -353,6 +353,10 @@ class UnifiedJobTypeStringMixin(object): def _underscore_to_camel(cls, word): return ''.join(x.capitalize() or '_' for x in word.split('_')) + @classmethod + def _camel_to_underscore(cls, word): + return re.sub('(?!^)([A-Z]+)', r'_\1', word).lower() + @classmethod def _model_type(cls, job_type): # Django >= 1.9 @@ -371,6 +375,9 @@ class UnifiedJobTypeStringMixin(object): return None return model.objects.get(id=job_id) + def model_to_str(self): + return UnifiedJobTypeStringMixin._camel_to_underscore(self.__class__.__name__) + class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique, UnifiedJobTypeStringMixin): ''' diff --git a/awx/main/scheduler/__init__.py b/awx/main/scheduler/__init__.py index 8569fb5cfc..a50a4177ac 100644 --- a/awx/main/scheduler/__init__.py +++ b/awx/main/scheduler/__init__.py @@ -166,6 +166,9 @@ class TaskManager(): return (active_task_queues, active_tasks) + def get_dependent_jobs_for_inv_and_proj_update(self, job_obj): + return [{'type': j.model_to_str(), 'id': j.id} for j in job_obj.dependent_jobs.all()] + def start_task(self, task, dependent_tasks=[]): from awx.main.tasks import handle_work_error, handle_work_success @@ -179,6 +182,17 @@ class TaskManager(): success_handler = handle_work_success.s(task_actual=task_actual) job_obj = task.get_full() + ''' + This is to account for when there isn't enough capacity to execute all + dependent jobs (i.e. proj or inv update) within the same schedule() + call. + + Proceeding calls to schedule() need to recontruct the proj or inv + update -> job fail logic dependency. The below call recontructs that + failure dependency. + ''' + if len(dependencies) == 0: + dependencies = self.get_dependent_jobs_for_inv_and_proj_update(job_obj) job_obj.status = 'waiting' (start_status, opts) = job_obj.pre_start() @@ -230,10 +244,32 @@ class TaskManager(): return inventory_task + ''' + Since we are dealing with partial objects we don't get to take advantage + of Django to resolve the type of related Many to Many field dependent_job. + + Hence the, potentional, double query in this method. + ''' + def get_related_dependent_jobs_as_patials(self, job_ids): + dependent_partial_jobs = [] + for id in job_ids: + if ProjectUpdate.objects.filter(id=id).exists(): + dependent_partial_jobs.append(ProjectUpdateDict({"id": id}).refresh_partial()) + elif InventoryUpdate.objects.filter(id=id).exists(): + dependent_partial_jobs.append(InventoryUpdateDict({"id": id}).refresh_partial()) + return dependent_partial_jobs + + def capture_chain_failure_dependencies(self, task, dependencies): + for dep in dependencies: + dep_obj = task.get_full() + dep_obj.dependent_jobs.add(task['id']) + dep_obj.save() + def generate_dependencies(self, task): dependencies = [] # TODO: What if the project is null ? if type(task) is JobDict: + if task['project__scm_update_on_launch'] is True and \ self.graph.should_update_related_project(task): project_task = self.create_project_update(task) @@ -248,6 +284,8 @@ class TaskManager(): if self.graph.should_update_related_inventory_source(task, inventory_source_task['id']): inventory_task = self.create_inventory_update(task, inventory_source_task) dependencies.append(inventory_task) + + self.capture_chain_failure_dependencies(task, dependencies) return dependencies def process_latest_project_updates(self, latest_project_updates): diff --git a/awx/main/scheduler/partial.py b/awx/main/scheduler/partial.py index d16634f369..03f07005dd 100644 --- a/awx/main/scheduler/partial.py +++ b/awx/main/scheduler/partial.py @@ -1,6 +1,7 @@ # Python import json +import itertools # AWX from awx.main.utils import decrypt_field_value @@ -61,13 +62,36 @@ class PartialModelDict(object): def task_impact(self): raise RuntimeError("Inherit and implement me") + @classmethod + def merge_values(cls, values): + grouped_results = itertools.groupby(values, key=lambda value: value['id']) + + merged_values = [] + for k, g in grouped_results: + print k + groups = list(g) + merged_value = {} + for group in groups: + for key, val in group.iteritems(): + if not merged_value.get(key): + merged_value[key] = val + elif val != merged_value[key]: + if isinstance(merged_value[key], list): + if val not in merged_value[key]: + merged_value[key].append(val) + else: + old_val = merged_value[key] + merged_value[key] = [old_val, val] + merged_values.append(merged_value) + return merged_values + class JobDict(PartialModelDict): FIELDS = ( 'id', 'status', 'job_template_id', 'inventory_id', 'project_id', 'launch_type', 'limit', 'allow_simultaneous', 'created', 'job_type', 'celery_task_id', 'project__scm_update_on_launch', - 'forks', 'start_args', + 'forks', 'start_args', 'dependent_jobs__id', ) model = Job @@ -85,6 +109,14 @@ class JobDict(PartialModelDict): start_args = start_args or {} return start_args.get('inventory_sources_already_updated', []) + @classmethod + def filter_partial(cls, status=[]): + kv = { + 'status__in': status + } + merged = PartialModelDict.merge_values(cls.model.objects.filter(**kv).values(*cls.get_db_values())) + return [cls(o) for o in merged] + class ProjectUpdateDict(PartialModelDict): FIELDS = ( @@ -134,7 +166,8 @@ class InventoryUpdateDict(PartialModelDict): #'inventory_source__update_on_launch', #'inventory_source__update_cache_timeout', FIELDS = ( - 'id', 'status', 'created', 'celery_task_id', 'inventory_source_id', 'inventory_source__inventory_id', + 'id', 'status', 'created', 'celery_task_id', 'inventory_source_id', + 'inventory_source__inventory_id', ) model = InventoryUpdate @@ -217,7 +250,7 @@ class SystemJobDict(PartialModelDict): class AdHocCommandDict(PartialModelDict): FIELDS = ( - 'id', 'created', 'status', 'inventory_id', + 'id', 'created', 'status', 'inventory_id', 'dependent_jobs__id', ) model = AdHocCommand diff --git a/awx/main/tests/unit/scheduler/conftest.py b/awx/main/tests/unit/scheduler/conftest.py index f04ba12a0b..40e221d0cc 100644 --- a/awx/main/tests/unit/scheduler/conftest.py +++ b/awx/main/tests/unit/scheduler/conftest.py @@ -36,6 +36,7 @@ def scheduler_factory(mocker, epoch): def no_create_project_update(task): raise RuntimeError("create_project_update should not be called") + mocker.patch.object(sched, 'capture_chain_failure_dependencies') mocker.patch.object(sched, 'get_tasks', return_value=tasks) mocker.patch.object(sched, 'get_running_workflow_jobs', return_value=[]) mocker.patch.object(sched, 'get_inventory_source_tasks', return_value=inventory_sources) diff --git a/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py b/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py index 5e49eec729..e337d0fd9c 100644 --- a/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py +++ b/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py @@ -87,3 +87,21 @@ class TestCreateDependentInventoryUpdate(): scheduler._schedule() scheduler.start_task.assert_called_with(waiting_inventory_update, [pending_job]) + + +class TestCaptureChainFailureDependencies(): + @pytest.fixture + def inventory_id_sources(self, inventory_source_factory): + return [ + (1, [inventory_source_factory(id=1)]), + ] + + def test(self, scheduler_factory, pending_job, waiting_inventory_update, inventory_id_sources): + scheduler = scheduler_factory(tasks=[pending_job], + create_inventory_update=waiting_inventory_update, + inventory_sources=inventory_id_sources) + + scheduler._schedule() + + scheduler.capture_chain_failure_dependencies.assert_called_with(pending_job, [waiting_inventory_update]) + From 812b7c5f5f4f163cf6f8085017b9c05110bb4d98 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 9 Dec 2016 13:27:58 -0500 Subject: [PATCH 067/595] fix two inv updates created from 1 jt run --- awx/main/scheduler/__init__.py | 3 ++ awx/main/scheduler/dependency_graph.py | 10 +++---- awx/main/scheduler/partial.py | 1 + .../test_scheduler_inventory_update.py | 29 +++++++++++++++++-- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/awx/main/scheduler/__init__.py b/awx/main/scheduler/__init__.py index a50a4177ac..efba3fdb75 100644 --- a/awx/main/scheduler/__init__.py +++ b/awx/main/scheduler/__init__.py @@ -276,6 +276,9 @@ class TaskManager(): dependencies.append(project_task) # Inventory created 2 seconds behind job + ''' + Inventory may have already been synced from a provision callback. + ''' inventory_sources_already_updated = task.get_inventory_sources_already_updated() for inventory_source_task in self.graph.get_inventory_sources(task['inventory_id']): diff --git a/awx/main/scheduler/dependency_graph.py b/awx/main/scheduler/dependency_graph.py index 7b66a8fe1b..846a194b27 100644 --- a/awx/main/scheduler/dependency_graph.py +++ b/awx/main/scheduler/dependency_graph.py @@ -117,10 +117,6 @@ class DependencyGraph(object): if not latest_inventory_update: return True - # TODO: Other finished, failed cases? i.e. error ? - if latest_inventory_update['status'] in ['failed', 'canceled']: - return True - ''' This is a bit of fuzzy logic. If the latest inventory update has a created time == job_created_time-2 @@ -138,7 +134,11 @@ class DependencyGraph(object): timeout_seconds = timedelta(seconds=latest_inventory_update['inventory_source__update_cache_timeout']) if (latest_inventory_update['finished'] + timeout_seconds) < now: return True - + + if latest_inventory_update['inventory_source__update_on_launch'] is True and \ + latest_inventory_update['status'] in ['failed', 'canceled', 'error']: + return True + return False def mark_system_job(self): diff --git a/awx/main/scheduler/partial.py b/awx/main/scheduler/partial.py index 03f07005dd..50c8d6653b 100644 --- a/awx/main/scheduler/partial.py +++ b/awx/main/scheduler/partial.py @@ -184,6 +184,7 @@ class InventoryUpdateLatestDict(InventoryUpdateDict): FIELDS = ( 'id', 'status', 'created', 'celery_task_id', 'inventory_source_id', 'finished', 'inventory_source__update_cache_timeout', 'launch_type', + 'inventory_source__update_on_launch', ) model = InventoryUpdate diff --git a/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py b/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py index e337d0fd9c..acffff3f8d 100644 --- a/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py +++ b/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py @@ -26,6 +26,22 @@ def successful_inventory_update_latest_cache_expired(inventory_update_latest_fac return iu +@pytest.fixture +def failed_inventory_update_latest_cache_zero(failed_inventory_update_latest): + iu = failed_inventory_update_latest + iu['inventory_source__update_cache_timeout'] = 0 + iu['inventory_source__update_on_launch'] = True + iu['finished'] = iu['created'] + timedelta(seconds=2) + iu['status'] = 'failed' + return iu + + +@pytest.fixture +def failed_inventory_update_latest_cache_non_zero(failed_inventory_update_latest_cache_zero): + failed_inventory_update_latest_cache_zero['inventory_source__update_cache_timeout'] = 10000000 + return failed_inventory_update_latest_cache_zero + + class TestStartInventoryUpdate(): def test_pending(self, scheduler_factory, pending_inventory_update): scheduler = scheduler_factory(tasks=[pending_inventory_update]) @@ -79,9 +95,18 @@ class TestCreateDependentInventoryUpdate(): scheduler.start_task.assert_called_with(waiting_inventory_update, [pending_job]) - def test_last_update_failed(self, scheduler_factory, pending_job, failed_inventory_update, failed_inventory_update_latest, waiting_inventory_update, inventory_id_sources): + def test_last_update_timeout_zero_failed(self, scheduler_factory, pending_job, failed_inventory_update, failed_inventory_update_latest_cache_zero, waiting_inventory_update, inventory_id_sources): scheduler = scheduler_factory(tasks=[failed_inventory_update, pending_job], - latest_inventory_updates=[failed_inventory_update_latest], + latest_inventory_updates=[failed_inventory_update_latest_cache_zero], + create_inventory_update=waiting_inventory_update, + inventory_sources=inventory_id_sources) + scheduler._schedule() + + scheduler.start_task.assert_called_with(waiting_inventory_update, [pending_job]) + + def test_last_update_timeout_non_zero_failed(self, scheduler_factory, pending_job, failed_inventory_update, failed_inventory_update_latest_cache_non_zero, waiting_inventory_update, inventory_id_sources): + scheduler = scheduler_factory(tasks=[failed_inventory_update, pending_job], + latest_inventory_updates=[failed_inventory_update_latest_cache_non_zero], create_inventory_update=waiting_inventory_update, inventory_sources=inventory_id_sources) scheduler._schedule() From 40d5142d11b8c628c19be9998c15f61c684c3a3a Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 9 Dec 2016 14:19:41 -0500 Subject: [PATCH 068/595] remove more mongo things --- awx/api/management/commands/uses_mongo.py | 58 ----------------------- 1 file changed, 58 deletions(-) delete mode 100644 awx/api/management/commands/uses_mongo.py diff --git a/awx/api/management/commands/uses_mongo.py b/awx/api/management/commands/uses_mongo.py deleted file mode 100644 index 6f77ee47fa..0000000000 --- a/awx/api/management/commands/uses_mongo.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved - -import sys - -from optparse import make_option -from django.core.management.base import BaseCommand -from awx.main.ha import is_ha_environment -from awx.main.task_engine import TaskEnhancer - - -class Command(BaseCommand): - """Return a exit status of 0 if MongoDB should be active, and an - exit status of 1 otherwise. - - This script is intended to be used by bash and init scripts to - conditionally start MongoDB, so its focus is on being bash-friendly. - """ - - def __init__(self): - super(Command, self).__init__() - BaseCommand.option_list += (make_option('--local', - dest='local', - default=False, - action="store_true", - help="Only check if mongo should be running locally"),) - - def handle(self, *args, **kwargs): - # Get the license data. - license_data = TaskEnhancer().validate_enhancements() - - # Does the license have features, at all? - # If there is no license yet, then all features are clearly off. - if 'features' not in license_data: - print('No license available.') - sys.exit(2) - - # Does the license contain the system tracking feature? - # If and only if it does, MongoDB should run. - system_tracking = license_data['features']['system_tracking'] - - # Okay, do we need MongoDB to be turned on? - # This is a silly variable assignment right now, but I expect the - # rules here will grow more complicated over time. - uses_mongo = system_tracking # noqa - - if is_ha_environment() and kwargs['local'] and uses_mongo: - print("HA Configuration detected. Database should be remote") - uses_mongo = False - - # If we do not need Mongo, return a non-zero exit status. - if not uses_mongo: - print('MongoDB NOT required') - sys.exit(1) - - # We do need Mongo, return zero. - print('MongoDB required') - sys.exit(0) From 56d8fa8ae64c1b05fb5d4fba435c9a14c6f66485 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Fri, 9 Dec 2016 14:29:46 -0500 Subject: [PATCH 069/595] Update project help for ConfigureTowerInTower. --- awx/ui/client/src/forms/Projects.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/forms/Projects.js b/awx/ui/client/src/forms/Projects.js index a1d65a82d5..ece9701855 100644 --- a/awx/ui/client/src/forms/Projects.js +++ b/awx/ui/client/src/forms/Projects.js @@ -79,7 +79,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition']) ngShow: "scm_type.value == 'manual' " , awPopOver: '

' + i18n._('Base path used for locating playbooks. Directories found inside this path will be listed in the playbook directory drop-down. ' + 'Together the base path and selected playbook directory provide the full path used to locate playbooks.') + '

' + - '

' + i18n.sprintf(i18n._('Use %s in your environment settings file to determine the base path value.'), 'PROJECTS_ROOT') + '

', + '

' + i18n.sprintf(i18n._('Change %s under "Configure Tower" to change this location.'), 'PROJECTS_ROOT') + '

', dataTitle: i18n._('Project Base Path'), dataContainer: 'body', dataPlacement: 'right', @@ -95,9 +95,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition']) init: false }, ngShow: "scm_type.value == 'manual' && !showMissingPlaybooksAlert", - awPopOver: '

' + i18n._('Select from the list of directories found in the base path.' + - 'Together the base path and the playbook directory provide the full path used to locate playbooks.') + '

' + - '

' + i18n.sprintf(i18n._('Use %s in your environment settings file to determine the base path value.'), 'PROJECTS_ROOT') + '

', + awPopOver: '

' + i18n._('Select from the list of directories found in the Project Base Path. ' + + 'Together the base path and the playbook directory provide the full path used to locate playbooks.') + '

', dataTitle: i18n._('Project Path'), dataContainer: 'body', dataPlacement: 'right', From 35fe4c8448e629350f40d92fe3253c06cbbaaa6f Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 9 Dec 2016 14:34:32 -0500 Subject: [PATCH 070/595] remove pyzmq --- .../commands/run_socketio_service.py | 293 ------------------ awx/main/socket_queue.py | 169 ---------- requirements/requirements.in | 1 - requirements/requirements.txt | 1 - 4 files changed, 464 deletions(-) delete mode 100644 awx/main/management/commands/run_socketio_service.py delete mode 100644 awx/main/socket_queue.py diff --git a/awx/main/management/commands/run_socketio_service.py b/awx/main/management/commands/run_socketio_service.py deleted file mode 100644 index 9b7e5a61d2..0000000000 --- a/awx/main/management/commands/run_socketio_service.py +++ /dev/null @@ -1,293 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Python -import os -import logging -import urllib -import weakref -from optparse import make_option -from threading import Thread - -# Django -from django.conf import settings -from django.core.management.base import NoArgsCommand - -# AWX -import awx -from awx.main.models import * # noqa -from awx.main.socket_queue import Socket - -# socketio -from socketio import socketio_manage -from socketio.server import SocketIOServer -from socketio.namespace import BaseNamespace - -logger = logging.getLogger('awx.main.commands.run_socketio_service') - - -class SocketSession(object): - def __init__(self, session_id, token_key, socket): - self.socket = weakref.ref(socket) - self.session_id = session_id - self.token_key = token_key - self._valid = True - - def is_valid(self): - return bool(self._valid) - - def invalidate(self): - self._valid = False - - def is_db_token_valid(self): - auth_token = AuthToken.objects.filter(key=self.token_key, reason='') - if not auth_token.exists(): - return False - auth_token = auth_token[0] - return bool(not auth_token.is_expired()) - - -class SocketSessionManager(object): - def __init__(self): - self.SESSIONS_MAX = 1000 - self.socket_sessions = [] - self.socket_session_token_key_map = {} - - def _prune(self): - if len(self.socket_sessions) > self.SESSIONS_MAX: - session = self.socket_sessions[0] - entries = self.socket_session_token_key_map[session.token_key] - del entries[session.session_id] - if len(entries) == 0: - del self.socket_session_token_key_map[session.token_key] - self.socket_sessions.pop(0) - - ''' - Returns an dict of sessions - ''' - def lookup(self, token_key=None): - if not token_key: - raise ValueError("token_key required") - return self.socket_session_token_key_map.get(token_key, None) - - def add_session(self, session): - self.socket_sessions.append(session) - entries = self.socket_session_token_key_map.get(session.token_key, None) - if not entries: - entries = {} - self.socket_session_token_key_map[session.token_key] = entries - entries[session.session_id] = session - self._prune() - return session - - -class SocketController(object): - def __init__(self, SocketSessionManager): - self.server = None - self.SocketSessionManager = SocketSessionManager - - def add_session(self, session): - return self.SocketSessionManager.add_session(session) - - def broadcast_packet(self, packet): - # Broadcast message to everyone at endpoint - # Loop over the 'raw' list of sockets (don't trust our list) - for session_id, socket in list(self.server.sockets.iteritems()): - socket_session = socket.session.get('socket_session', None) - if socket_session and socket_session.is_valid(): - try: - socket.send_packet(packet) - except Exception as e: - logger.error("Error sending client packet to %s: %s" % (str(session_id), str(packet))) - logger.error("Error was: " + str(e)) - - def send_packet(self, packet, token_key): - if not token_key: - raise ValueError("token_key is required") - socket_sessions = self.SocketSessionManager.lookup(token_key=token_key) - # We may not find the socket_session if the user disconnected - # (it's actually more compliciated than that because of our prune logic) - if not socket_sessions: - return None - for session_id, socket_session in socket_sessions.iteritems(): - logger.warn("Maybe sending packet to %s" % session_id) - if socket_session and socket_session.is_valid(): - logger.warn("Sending packet to %s" % session_id) - socket = socket_session.socket() - if socket: - try: - socket.send_packet(packet) - except Exception as e: - logger.error("Error sending client packet to %s: %s" % (str(socket_session.session_id), str(packet))) - logger.error("Error was: " + str(e)) - - def set_server(self, server): - self.server = server - return server - - -socketController = SocketController(SocketSessionManager()) - - -# -# Socket session is attached to self.session['socket_session'] -# self.session and self.socket.session point to the same dict -# -class TowerBaseNamespace(BaseNamespace): - def get_allowed_methods(self): - return ['recv_disconnect'] - - def get_initial_acl(self): - request_token = self._get_request_token() - if request_token: - # (1) This is the first time the socket has been seen (first - # namespace joined). - # (2) This socket has already been seen (already joined and maybe - # left a namespace) - # - # Note: Assume that the user token is valid if the session is found - socket_session = self.session.get('socket_session', None) - if not socket_session: - socket_session = SocketSession(self.socket.sessid, request_token, self.socket) - if socket_session.is_db_token_valid(): - self.session['socket_session'] = socket_session - socketController.add_session(socket_session) - else: - socket_session.invalidate() - - return set(['recv_connect'] + self.get_allowed_methods()) - else: - logger.warn("Authentication Failure validating user") - self.emit("connect_failed", "Authentication failed") - return set(['recv_connect']) - - def _get_request_token(self): - if 'QUERY_STRING' not in self.environ: - return False - - try: - k, v = self.environ['QUERY_STRING'].split("=") - if k == "Token": - token_actual = urllib.unquote_plus(v).decode().replace("\"","") - return token_actual - except Exception as e: - logger.error("Exception validating user: " + str(e)) - return False - return False - - def recv_connect(self): - socket_session = self.session.get('socket_session', None) - if socket_session and not socket_session.is_valid(): - self.disconnect(silent=False) - - -class TestNamespace(TowerBaseNamespace): - def recv_connect(self): - logger.info("Received client connect for test namespace from %s" % str(self.environ['REMOTE_ADDR'])) - self.emit('test', "If you see this then you attempted to connect to the test socket endpoint") - super(TestNamespace, self).recv_connect() - - -class JobNamespace(TowerBaseNamespace): - def recv_connect(self): - logger.info("Received client connect for job namespace from %s" % str(self.environ['REMOTE_ADDR'])) - super(JobNamespace, self).recv_connect() - - -class JobEventNamespace(TowerBaseNamespace): - def recv_connect(self): - logger.info("Received client connect for job event namespace from %s" % str(self.environ['REMOTE_ADDR'])) - super(JobEventNamespace, self).recv_connect() - - -class AdHocCommandEventNamespace(TowerBaseNamespace): - def recv_connect(self): - logger.info("Received client connect for ad hoc command event namespace from %s" % str(self.environ['REMOTE_ADDR'])) - super(AdHocCommandEventNamespace, self).recv_connect() - - -class ScheduleNamespace(TowerBaseNamespace): - def get_allowed_methods(self): - parent_allowed = super(ScheduleNamespace, self).get_allowed_methods() - return parent_allowed + ["schedule_changed"] - - def recv_connect(self): - logger.info("Received client connect for schedule namespace from %s" % str(self.environ['REMOTE_ADDR'])) - super(ScheduleNamespace, self).recv_connect() - - -# Catch-all namespace. -# Deliver 'global' events over this namespace -class ControlNamespace(TowerBaseNamespace): - def recv_connect(self): - logger.warn("Received client connect for control namespace from %s" % str(self.environ['REMOTE_ADDR'])) - super(ControlNamespace, self).recv_connect() - - -class TowerSocket(object): - def __call__(self, environ, start_response): - path = environ['PATH_INFO'].strip('/') or 'index.html' - if path.startswith('socket.io'): - socketio_manage(environ, {'/socket.io/test': TestNamespace, - '/socket.io/jobs': JobNamespace, - '/socket.io/job_events': JobEventNamespace, - '/socket.io/ad_hoc_command_events': AdHocCommandEventNamespace, - '/socket.io/schedules': ScheduleNamespace, - '/socket.io/control': ControlNamespace}) - else: - logger.warn("Invalid connect path received: " + path) - start_response('404 Not Found', []) - return ['Tower version %s' % awx.__version__] - - -def notification_handler(server): - with Socket('websocket', 'r') as websocket: - for message in websocket.listen(): - packet = { - 'args': message, - 'endpoint': message['endpoint'], - 'name': message['event'], - 'type': 'event', - } - - if 'token_key' in message: - # Best practice not to send the token over the socket - socketController.send_packet(packet, message.pop('token_key')) - else: - socketController.broadcast_packet(packet) - - -class Command(NoArgsCommand): - ''' - SocketIO event emitter Tower service - Receives notifications from other services destined for UI notification - ''' - - help = 'Launch the SocketIO event emitter service' - - option_list = NoArgsCommand.option_list + ( - make_option('--receive_port', dest='receive_port', type='int', default=5559, - help='Port to listen for new events that will be destined for a client'), - make_option('--socketio_port', dest='socketio_port', type='int', default=8080, - help='Port to accept socketio requests from clients'),) - - def handle_noargs(self, **options): - socketio_listen_port = settings.SOCKETIO_LISTEN_PORT - - try: - if os.path.exists('/etc/tower/tower.cert') and os.path.exists('/etc/tower/tower.key'): - logger.info('Listening on port https://0.0.0.0:' + str(socketio_listen_port)) - server = SocketIOServer(('0.0.0.0', socketio_listen_port), TowerSocket(), resource='socket.io', - keyfile='/etc/tower/tower.key', certfile='/etc/tower/tower.cert') - else: - logger.info('Listening on port http://0.0.0.0:' + str(socketio_listen_port)) - server = SocketIOServer(('0.0.0.0', socketio_listen_port), TowerSocket(), resource='socket.io') - - socketController.set_server(server) - handler_thread = Thread(target=notification_handler, args=(server,)) - handler_thread.daemon = True - handler_thread.start() - - server.serve_forever() - except KeyboardInterrupt: - pass diff --git a/awx/main/socket_queue.py b/awx/main/socket_queue.py deleted file mode 100644 index 40dba76366..0000000000 --- a/awx/main/socket_queue.py +++ /dev/null @@ -1,169 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -import os - -import zmq - -from django.conf import settings - - -class Socket(object): - """An abstraction class implemented for a dumb OS socket. - - Intended to allow alteration of backend details in a single, consistent - way throughout the Tower application. - """ - def __init__(self, bucket, rw, debug=0, logger=None, nowait=False): - """Instantiate a Socket object, which uses ZeroMQ to actually perform - passing a message back and forth. - - Designed to be used as a context manager: - - with Socket('callbacks', 'w') as socket: - socket.publish({'message': 'foo bar baz'}) - - If listening for messages through a socket, the `listen` method - is a simple generator: - - with Socket('callbacks', 'r') as socket: - for message in socket.listen(): - [...] - """ - self._bucket = bucket - self._rw = { - 'r': zmq.REP, - 'w': zmq.REQ, - }[rw.lower()] - - self._connection_pid = None - self._context = None - self._socket = None - - self._debug = debug - self._logger = logger - self._nowait = nowait - - def __enter__(self): - self.connect() - return self - - def __exit__(self, *args, **kwargs): - self.close() - - @property - def is_connected(self): - if self._socket: - return True - return False - - @property - def port(self): - return { - 'callbacks': os.environ.get('CALLBACK_CONSUMER_PORT', - getattr(settings, 'CALLBACK_CONSUMER_PORT', 'tcp://127.0.0.1:5557')), - 'task_commands': settings.TASK_COMMAND_PORT, - 'websocket': settings.SOCKETIO_NOTIFICATION_PORT, - 'fact_cache': settings.FACT_CACHE_PORT, - }[self._bucket] - - def connect(self): - """Connect to ZeroMQ.""" - - # Make sure that we are clearing everything out if there is - # a problem; PID crossover can cause bad news. - active_pid = os.getpid() - if self._connection_pid is None: - self._connection_pid = active_pid - if self._connection_pid != active_pid: - self._context = None - self._socket = None - self._connection_pid = active_pid - - # If the port is an integer, convert it into tcp:// - port = self.port - if isinstance(port, int): - port = 'tcp://127.0.0.1:%d' % port - - # If the port is None, then this is an intentional dummy; - # honor this. (For testing.) - if not port: - return - - # Okay, create the connection. - if self._context is None: - self._context = zmq.Context() - self._socket = self._context.socket(self._rw) - if self._nowait: - self._socket.setsockopt(zmq.RCVTIMEO, 2000) - self._socket.setsockopt(zmq.LINGER, 1000) - if self._rw == zmq.REQ: - self._socket.connect(port) - else: - self._socket.bind(port) - - def close(self): - """Disconnect and tear down.""" - if self._socket: - self._socket.close() - self._socket = None - self._context = None - - def publish(self, message): - """Publish a message over the socket.""" - - # If the port is None, no-op. - if self.port is None: - return - - # If we are not connected, whine. - if not self.is_connected: - raise RuntimeError('Cannot publish a message when not connected ' - 'to the socket.') - - # If we are in the wrong mode, whine. - if self._rw != zmq.REQ: - raise RuntimeError('This socket is not opened for writing.') - - # If we are in debug mode; provide the PID. - if self._debug: - message.update({'pid': os.getpid(), - 'connection_pid': self._connection_pid}) - - # Send the message. - for retry in xrange(4): - try: - self._socket.send_json(message) - self._socket.recv() - break - except Exception as ex: - if self._logger: - self._logger.error('Publish Exception: %r; retry=%d', - ex, retry, exc_info=True) - if retry >= 3: - raise - - def listen(self): - """Retrieve a single message from the subcription channel - and return it. - """ - # If the port is None, no-op. - if self.port is None: - raise StopIteration - - # If we are not connected, whine. - if not self.is_connected: - raise RuntimeError('Cannot publish a message when not connected ' - 'to the socket.') - - # If we are in the wrong mode, whine. - if self._rw != zmq.REP: - raise RuntimeError('This socket is not opened for reading.') - - # Actually listen to the socket. - while True: - try: - message = self._socket.recv_json() - yield message - finally: - self._socket.send('1') diff --git a/requirements/requirements.in b/requirements/requirements.in index 886476fecb..3ae18db466 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -38,7 +38,6 @@ python-memcached==1.58 python-radius==1.0 python-saml==2.2.0 python-social-auth==0.2.21 -pyzmq==14.5.0 redbaron==0.6.2 requests-futures==0.9.7 shade==1.13.1 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e39a8aed88..03a6d9f614 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -164,7 +164,6 @@ python-swiftclient==3.2.0 # via python-heatclient, python-troveclient, shade python-troveclient==2.6.0 # via shade pytz==2016.7 # via babel, celery, irc, oslo.serialization, oslo.utils, tempora, twilio PyYAML==3.12 # via cliff, djangorestframework-yaml, os-client-config, psphere, python-heatclient, python-ironicclient, python-mistralclient -pyzmq==14.5.0 rackspace-auth-openstack==1.3 # via rackspace-novaclient rackspace-novaclient==2.1 rax-default-network-flags-python-novaclient-ext==0.4.0 # via rackspace-novaclient From 5ff62c97b69ac5352734f87e7ec4a764cfcade97 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 6 Dec 2016 19:18:14 -0500 Subject: [PATCH 071/595] uwsgi auto-reload on logging change --- Makefile | 2 +- awx/main/tasks.py | 11 +++++------ awx/settings/defaults.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 2e29eb174d..496b520775 100644 --- a/Makefile +++ b/Makefile @@ -406,7 +406,7 @@ uwsgi: collectstatic @if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/tower/bin/activate; \ fi; \ - uwsgi -b 32768 --socket :8050 --module=awx.wsgi:application --home=/venv/tower --chdir=/tower_devel/ --vacuum --processes=5 --harakiri=60 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket + uwsgi -b 32768 --socket :8050 --module=awx.wsgi:application --home=/venv/tower --chdir=/tower_devel/ --vacuum --processes=5 --harakiri=60 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --master-fifo=/tmp/awxfifo daphne: @if [ "$(VENV_BASE)" ]; then \ diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 5e78750f3a..7ca90949a1 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -85,6 +85,10 @@ def celery_startup(conf=None, **kwargs): logger.error("Failed to rebuild schedule {}: {}".format(sch, e)) +def uwsgi_reload(): + os.system("echo r > /tmp/awxfifo") + + @task(queue='broadcast_all') def clear_cache_keys(cache_keys): set_of_keys = set([key for key in cache_keys]) @@ -92,12 +96,7 @@ def clear_cache_keys(cache_keys): cache.delete_many(set_of_keys) for setting_key in set_of_keys: if setting_key.startswith('LOG_AGGREGATOR_'): - LOGGING = settings.LOGGING - if settings.LOG_AGGREGATOR_ENABLED: - LOGGING['handlers']['http_receiver']['class'] = 'awx.main.utils.handlers.HTTPSHandler' - else: - LOGGING['handlers']['http_receiver']['class'] = 'awx.main.utils.handlers.HTTPSNullHandler' - configure_logging(settings.LOGGING_CONFIG, LOGGING) + uwsgi_reload() break diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 6aa30f252b..b9fa948521 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -999,7 +999,7 @@ LOGGING = { 'propagate': False, }, 'awx.analytics': { - 'handlers': ['null'], + 'handlers': ['http_receiver'], 'level': 'INFO', 'propagate': False }, From 431dcc6490c2d25d73ae658d58a682c432dc0638 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 9 Dec 2016 15:38:38 -0500 Subject: [PATCH 072/595] switch to smoother chain reloading --- Makefile | 2 +- awx/main/tasks.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 496b520775..8bf3766abd 100644 --- a/Makefile +++ b/Makefile @@ -406,7 +406,7 @@ uwsgi: collectstatic @if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/tower/bin/activate; \ fi; \ - uwsgi -b 32768 --socket :8050 --module=awx.wsgi:application --home=/venv/tower --chdir=/tower_devel/ --vacuum --processes=5 --harakiri=60 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --master-fifo=/tmp/awxfifo + uwsgi -b 32768 --socket :8050 --module=awx.wsgi:application --home=/venv/tower --chdir=/tower_devel/ --vacuum --processes=5 --harakiri=60 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --master-fifo=/tmp/awxfifo --lazy-apps daphne: @if [ "$(VENV_BASE)" ]; then \ diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 7ca90949a1..82a92f43fb 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -86,7 +86,8 @@ def celery_startup(conf=None, **kwargs): def uwsgi_reload(): - os.system("echo r > /tmp/awxfifo") + "Does chain reload of uWSGI" + os.system("echo c > /tmp/awxfifo") @task(queue='broadcast_all') From c318b12428ea438827f881fbe8a5b99ea8d10b82 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 9 Dec 2016 16:03:10 -0500 Subject: [PATCH 073/595] do not check survey license for workflow job relaunch --- awx/main/access.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 55b0185e53..b1e9beebc6 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1624,8 +1624,6 @@ class WorkflowJobAccess(BaseAccess): def can_start(self, obj, validate_license=True): if validate_license: self.check_license() - if obj.survey_enabled: - self.check_license(feature='surveys') if self.user.is_superuser: return True From c6f3d498de863468644326a01e5fdeea8abfb4b2 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 9 Dec 2016 16:07:47 -0500 Subject: [PATCH 074/595] remove django configure_logging import --- awx/main/tasks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 82a92f43fb..8e1c5b9731 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -43,7 +43,6 @@ from django.core.mail import send_mail from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ from django.core.cache import cache -from django.utils.log import configure_logging # AWX from awx.main.constants import CLOUD_PROVIDERS From c3666752e6a73f1cd82f149a828095cc7af46200 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Sat, 10 Dec 2016 07:46:30 -0500 Subject: [PATCH 075/595] check for UJT start access when adding node --- awx/main/access.py | 3 ++- awx/main/tests/functional/test_rbac_workflow.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/awx/main/access.py b/awx/main/access.py index b1e9beebc6..5551e35dc6 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1362,7 +1362,6 @@ class SystemJobAccess(BaseAccess): return False # no relaunching of system jobs -# TODO: class WorkflowJobTemplateNodeAccess(BaseAccess): ''' I can see/use a WorkflowJobTemplateNode if I have read permission @@ -1409,6 +1408,8 @@ class WorkflowJobTemplateNodeAccess(BaseAccess): return True if not self.check_related('workflow_job_template', WorkflowJobTemplate, data, mandatory=True): return False + if not self.check_related('unified_job_template', UnifiedJobTemplate, data): + return False if not self.can_use_prompted_resources(data): return False return True diff --git a/awx/main/tests/functional/test_rbac_workflow.py b/awx/main/tests/functional/test_rbac_workflow.py index 80eae5af8b..3b8e8f4862 100644 --- a/awx/main/tests/functional/test_rbac_workflow.py +++ b/awx/main/tests/functional/test_rbac_workflow.py @@ -55,6 +55,21 @@ class TestWorkflowJobTemplateNodeAccess: access = WorkflowJobTemplateNodeAccess(org_admin) assert not access.can_change(wfjt_node, {'job_type': 'scan'}) + def test_add_JT_no_start_perm(self, wfjt, job_template, rando): + wfjt.admin_role.members.add(rando) + access = WorkflowJobTemplateAccess(rando) + job_template.read_role.members.add(rando) + assert not access.can_add({ + 'workflow_job_template': wfjt.pk, + 'unified_job_template': job_template.pk}) + + def test_remove_unwanted_foreign_node(self, wfjt_node, job_template, rando): + wfjt = wfjt_node.workflow_job_template + wfjt.admin_role.members.add(rando) + wfjt_node.unified_job_template = job_template + access = WorkflowJobTemplateNodeAccess(rando) + assert access.can_delete(wfjt_node) + @pytest.mark.django_db class TestWorkflowJobAccess: From a90bafe6f082f9b734db4787caffb022cd46ea22 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Sun, 11 Dec 2016 07:31:04 -0500 Subject: [PATCH 076/595] move various items to prefetch for optimization --- awx/api/views.py | 2 ++ awx/main/access.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/awx/api/views.py b/awx/api/views.py index b82cb424d6..69dcbd064c 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1024,6 +1024,7 @@ class ProjectList(ListCreateAPIView): 'update_role', 'read_role', ) + projects_qs = projects_qs.prefetch_related('last_job', 'created_by') return projects_qs @@ -1579,6 +1580,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', 'adhoc_role') + qs = qs.prefetch_related('created_by', 'modified_by', 'organization') return qs diff --git a/awx/main/access.py b/awx/main/access.py index b1e9beebc6..95910fba20 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1808,6 +1808,9 @@ class UnifiedJobTemplateAccess(BaseAccess): 'created_by', 'modified_by', 'next_schedule', + ) + # prefetch last/current jobs so we get the real instance + qs = qs.prefetch_related( 'last_job', 'current_job', ) From 53349cc24db0e5e306c02cb3aac54cc6cc3cb6dd Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Mon, 12 Dec 2016 09:08:42 -0500 Subject: [PATCH 077/595] Fixed bug with loading and permissions, error logging and continuing after GET error --- awx/ui/client/src/configuration/configuration.service.js | 2 +- awx/ui/client/src/helpers/LoadConfig.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/configuration/configuration.service.js b/awx/ui/client/src/configuration/configuration.service.js index 9204638c8c..84dac656d0 100644 --- a/awx/ui/client/src/configuration/configuration.service.js +++ b/awx/ui/client/src/configuration/configuration.service.js @@ -16,7 +16,7 @@ export default ['$rootScope', 'GetBasePath', 'ProcessErrors', '$q', '$http', 'Re Rest.setUrl(url + '/all'); Rest.options() .success(function(data) { - if($rootScope.is_superuser) { + if($rootScope.user_is_superuser) { returnData = data.actions.PUT; } else { returnData = data.actions.GET; diff --git a/awx/ui/client/src/helpers/LoadConfig.js b/awx/ui/client/src/helpers/LoadConfig.js index a472c600a9..93d381079b 100644 --- a/awx/ui/client/src/helpers/LoadConfig.js +++ b/awx/ui/client/src/helpers/LoadConfig.js @@ -99,7 +99,8 @@ angular.module('LoadConfigHelper', ['Utilities']) configInit(); }).error(function(error) { - console.log(error); + $log.debug(error); + configInit(); }); }; From 4cc1642ab3dffcfc2f177dd24ed751bfd4ac6c8d Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 12 Dec 2016 09:40:49 -0500 Subject: [PATCH 078/595] clarity edit of uWSGI chain reload task --- awx/main/tasks.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 8e1c5b9731..e0dcae0fca 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -85,8 +85,11 @@ def celery_startup(conf=None, **kwargs): def uwsgi_reload(): - "Does chain reload of uWSGI" - os.system("echo c > /tmp/awxfifo") + # http://uwsgi-docs.readthedocs.io/en/latest/MasterFIFO.html#available-commands + logger.warn('Initiating uWSGI chain reload of server') + TRIGGER_CHAIN_RELOAD = 'c' + with open('/tmp/awxfifo', 'w') as awxfifo: + awxfifo.write(TRIGGER_CHAIN_RELOAD) @task(queue='broadcast_all') From 2d6dff1a3bec96b7cad4d6b4be2c14db274748e9 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 12 Dec 2016 10:54:00 -0500 Subject: [PATCH 079/595] Better error handling on deprovision_node management command --- awx/main/management/commands/deprovision_node.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/awx/main/management/commands/deprovision_node.py b/awx/main/management/commands/deprovision_node.py index 52b9e4f115..251816703e 100644 --- a/awx/main/management/commands/deprovision_node.py +++ b/awx/main/management/commands/deprovision_node.py @@ -1,7 +1,7 @@ # Copyright (c) 2016 Ansible, Inc. # All Rights Reserved -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, CommandError from optparse import make_option from awx.main.models import Instance @@ -16,8 +16,9 @@ class Command(BaseCommand): help='Hostname used during provisioning'), ) - def handle(self, **options): - # Get the instance. + def handle(self, *args, **options): + if not options.get('name'): + raise CommandError("--name is a required argument") instance = Instance.objects.filter(hostname=options.get('name')) if instance.exists(): instance.delete() From 30b212b724f59be1bdbb429bae1066b2d67cd224 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 12 Dec 2016 11:11:25 -0500 Subject: [PATCH 080/595] fix RBAC bugs associated with WFJT copy --- awx/api/views.py | 8 +++++--- awx/main/access.py | 2 +- awx/main/tests/functional/test_rbac_workflow.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index b82cb424d6..23d94ea336 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2888,11 +2888,13 @@ class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, GenericAPIView): def post(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(self.model, 'copy', obj): - return PermissionDenied() - new_wfjt = obj.user_copy(request.user) + raise PermissionDenied() + new_obj = obj.user_copy(request.user) + if request.user not in new_obj.admin_role: + new_obj.admin_role.members.add(request.user) data = OrderedDict() data.update(WorkflowJobTemplateSerializer( - new_wfjt, context=self.get_serializer_context()).to_representation(new_wfjt)) + new_obj, context=self.get_serializer_context()).to_representation(new_obj)) return Response(data, status=status.HTTP_201_CREATED) diff --git a/awx/main/access.py b/awx/main/access.py index b1e9beebc6..2ddc66ae02 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1549,7 +1549,7 @@ class WorkflowJobTemplateAccess(BaseAccess): wfjt_errors[node.id] = node_errors self.messages.update(wfjt_errors) - return self.check_related('organization', Organization, {}, obj=obj, mandatory=True) + return self.check_related('organization', Organization, {'reference_obj': obj}, mandatory=True) def can_start(self, obj, validate_license=True): if validate_license: diff --git a/awx/main/tests/functional/test_rbac_workflow.py b/awx/main/tests/functional/test_rbac_workflow.py index 80eae5af8b..96137f315f 100644 --- a/awx/main/tests/functional/test_rbac_workflow.py +++ b/awx/main/tests/functional/test_rbac_workflow.py @@ -71,6 +71,19 @@ class TestWorkflowJobAccess: access = WorkflowJobAccess(rando) assert access.can_cancel(workflow_job) + def test_copy_permissions_org_admin(self, wfjt, org_admin, org_member): + admin_access = WorkflowJobTemplateAccess(org_admin) + assert admin_access.can_copy(wfjt) + + def test_copy_permissions_user(self, wfjt, org_admin, org_member): + ''' + Only org admins are able to add WFJTs, only org admins + are able to copy them + ''' + wfjt.admin_role.members.add(org_member) + member_access = WorkflowJobTemplateAccess(org_member) + assert not member_access.can_copy(wfjt) + def test_workflow_copy_warnings_inv(self, wfjt, rando, inventory): ''' The user `rando` does not have access to the prompted inventory in a From 8165e18e645cf36d36a852c2c6846fe4c9ec8053 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 12 Dec 2016 11:19:59 -0500 Subject: [PATCH 081/595] Various small bug fixes dealing with the workflow maker and the add/edit workflow form. --- awx/ui/client/src/forms/JobTemplates.js | 2 +- awx/ui/client/src/forms/WorkflowMaker.js | 2 +- awx/ui/client/src/forms/Workflows.js | 3 +- awx/ui/client/src/shared/form-generator.js | 4 + .../shared/lookup/lookup-modal.partial.html | 2 +- awx/ui/client/src/templates/main.js | 9 +- .../edit-workflow/workflow-edit.controller.js | 504 ++++-------------- .../workflow-chart/workflow-chart.block.less | 8 + .../workflow-chart.directive.js | 83 ++- .../workflow-maker/workflow-maker.block.less | 4 +- .../workflow-maker.controller.js | 221 +++++++- .../workflow-maker.controller-test.js | 4 +- 12 files changed, 419 insertions(+), 427 deletions(-) diff --git a/awx/ui/client/src/forms/JobTemplates.js b/awx/ui/client/src/forms/JobTemplates.js index 9fe64f0469..c200258cc6 100644 --- a/awx/ui/client/src/forms/JobTemplates.js +++ b/awx/ui/client/src/forms/JobTemplates.js @@ -20,7 +20,7 @@ export default addTitle: i18n._('New Job Template'), editTitle: '{{ name }}', name: 'job_template', - breadcrumbName: 'JOB TEMPLATE', + breadcrumbName: i18n._('JOB TEMPLATE'), basePath: 'job_templates', // the top-most node of generated state tree stateTree: 'templates', diff --git a/awx/ui/client/src/forms/WorkflowMaker.js b/awx/ui/client/src/forms/WorkflowMaker.js index 5bd7788db1..9ed3c69b65 100644 --- a/awx/ui/client/src/forms/WorkflowMaker.js +++ b/awx/ui/client/src/forms/WorkflowMaker.js @@ -170,7 +170,7 @@ export default ngClick: 'cancelNodeForm()', ngShow: '!canAddWorkflowJobTemplate' }, - save: { + select: { ngClick: 'saveNodeForm()', ngDisabled: "workflow_maker_form.$invalid || !selectedTemplate", ngShow: 'canAddWorkflowJobTemplate' diff --git a/awx/ui/client/src/forms/Workflows.js b/awx/ui/client/src/forms/Workflows.js index d281ae0e0b..7f16ee6d06 100644 --- a/awx/ui/client/src/forms/Workflows.js +++ b/awx/ui/client/src/forms/Workflows.js @@ -16,9 +16,10 @@ export default .factory('WorkflowFormObject', ['i18n', function(i18n) { return { - addTitle: i18n._('New Workflow'), + addTitle: i18n._('New Workflow Job Template'), editTitle: '{{ name }}', name: 'workflow_job_template', + breadcrumbName: i18n._('WORKFLOW'), base: 'workflow', basePath: 'workflow_job_templates', // the top-most node of generated state tree diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index defdc6f5dc..d5850a9c8b 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1683,6 +1683,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat button.label = i18n._('Save'); button['class'] = 'Form-saveButton'; } + if (btn === 'select') { + button.label = i18n._('Select'); + button['class'] = 'Form-saveButton'; + } if (btn === 'cancel') { button.label = i18n._('Cancel'); button['class'] = 'Form-cancelButton'; diff --git a/awx/ui/client/src/shared/lookup/lookup-modal.partial.html b/awx/ui/client/src/shared/lookup/lookup-modal.partial.html index 5371e8d454..13d1a8b4dc 100644 --- a/awx/ui/client/src/shared/lookup/lookup-modal.partial.html +++ b/awx/ui/client/src/shared/lookup/lookup-modal.partial.html @@ -17,7 +17,7 @@
diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index f9ea226a83..b458121def 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -401,8 +401,10 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesA } }, resolve: { - ListDefinition: ['InventoryList', function(list) { + ListDefinition: ['InventoryList', function(InventoryList) { // mutate the provided list definition here + let list = _.cloneDeep(InventoryList); + list.lookupConfirmText = 'SELECT'; return list; }], Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', @@ -451,8 +453,9 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesA } }, resolve: { - ListDefinition: ['CredentialList', function(list) { - // mutate the provided list definition here + ListDefinition: ['CredentialList', function(CredentialList) { + let list = _.cloneDeep(CredentialList); + list.lookupConfirmText = 'SELECT'; return list; }], Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', diff --git a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js index e864beefa9..18dd35a37d 100644 --- a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js +++ b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js @@ -33,10 +33,6 @@ $scope.parseType = 'yaml'; $scope.includeWorkflowMaker = false; - $scope.editRequests = []; - $scope.associateRequests = []; - $scope.disassociateRequests = []; - function init() { // Select2-ify the lables input @@ -111,33 +107,6 @@ }); }); - // Get the workflow nodes - TemplatesService.getWorkflowJobTemplateNodes(id) - .then(function(data){ - - $scope.workflowTree = WorkflowService.buildTree({ - workflowNodes: data.data.results - }); - - // TODO: I think that the workflow chart directive (and eventually d3) is meddling with - // this workflowTree object and removing the children object for some reason (?) - // This happens on occasion and I think is a race condition (?) - if(!$scope.workflowTree.data.children) { - $scope.workflowTree.data.children = []; - } - - // In the partial, the workflow maker directive has an ng-if attribute which is pointed at this scope variable. - // It won't get included until this the tree has been built - I'm open to better ways of doing this. - $scope.includeWorkflowMaker = true; - - }, function(error){ - ProcessErrors($scope, error.data, error.status, form, { - hdr: 'Error!', - msg: 'Failed to get workflow job template nodes. GET returned ' + - 'status: ' + error.status - }); - }); - // Go out and GET the workflow job temlate data needed to populate the form TemplatesService.getWorkflowJobTemplate(id) .then(function(data){ @@ -180,6 +149,35 @@ $scope.url = workflowJobTemplateData.url; $scope.survey_enabled = workflowJobTemplateData.survey_enabled; + // Get the workflow nodes + TemplatesService.getWorkflowJobTemplateNodes(id) + .then(function(data){ + + $scope.workflowTree = WorkflowService.buildTree({ + workflowNodes: data.data.results + }); + + // TODO: I think that the workflow chart directive (and eventually d3) is meddling with + // this workflowTree object and removing the children object for some reason (?) + // This happens on occasion and I think is a race condition (?) + if(!$scope.workflowTree.data.children) { + $scope.workflowTree.data.children = []; + } + + $scope.workflowTree.workflow_job_template_obj = $scope.workflow_job_template_obj; + + // In the partial, the workflow maker directive has an ng-if attribute which is pointed at this scope variable. + // It won't get included until this the tree has been built - I'm open to better ways of doing this. + $scope.includeWorkflowMaker = true; + + }, function(error){ + ProcessErrors($scope, error.data, error.status, form, { + hdr: 'Error!', + msg: 'Failed to get workflow job template nodes. GET returned ' + + 'status: ' + error.status + }); + }); + }, function(error){ ProcessErrors($scope, error.data, error.status, form, { hdr: 'Error!', @@ -189,160 +187,6 @@ }); } - function recursiveNodeUpdates(params, completionCallback) { - // params.parentId - // params.node - - let generatePostUrl = function(){ - - let base = (params.parentId) ? GetBasePath('workflow_job_template_nodes') + params.parentId : $scope.workflow_job_template_obj.related.workflow_nodes; - - if(params.parentId) { - if(params.node.edgeType === 'success') { - base += "/success_nodes"; - } - else if(params.node.edgeType === 'failure') { - base += "/failure_nodes"; - } - else if(params.node.edgeType === 'always') { - base += "/always_nodes"; - } - } - - return base; - - }; - - let buildSendableNodeData = function() { - // Create the node - let sendableNodeData = { - unified_job_template: params.node.unifiedJobTemplate.id - }; - - // Check to see if the user has provided any prompt values that are different - // from the defaults in the job template - - if(params.node.unifiedJobTemplate.type === "job_template" && params.node.promptValues) { - if(params.node.unifiedJobTemplate.ask_credential_on_launch) { - sendableNodeData.credential = !params.node.promptValues.credential || params.node.unifiedJobTemplate.summary_fields.credential.id !== params.node.promptValues.credential.id ? params.node.promptValues.credential.id : null; - } - if(params.node.unifiedJobTemplate.ask_inventory_on_launch) { - sendableNodeData.inventory = !params.node.promptValues.inventory || params.node.unifiedJobTemplate.summary_fields.inventory.id !== params.node.promptValues.inventory.id ? params.node.promptValues.inventory.id : null; - } - if(params.node.unifiedJobTemplate.ask_limit_on_launch) { - sendableNodeData.limit = !params.node.promptValues.limit || params.node.unifiedJobTemplate.limit !== params.node.promptValues.limit ? params.node.promptValues.limit : null; - } - if(params.node.unifiedJobTemplate.ask_job_type_on_launch) { - sendableNodeData.job_type = !params.node.promptValues.job_type || params.node.unifiedJobTemplate.job_type !== params.node.promptValues.job_type ? params.node.promptValues.job_type : null; - } - if(params.node.unifiedJobTemplate.ask_tags_on_launch) { - sendableNodeData.job_tags = !params.node.promptValues.job_tags || params.node.unifiedJobTemplate.job_tags !== params.node.promptValues.job_tags ? params.node.promptValues.job_tags : null; - } - if(params.node.unifiedJobTemplate.ask_skip_tags_on_launch) { - sendableNodeData.skip_tags = !params.node.promptValues.skip_tags || params.node.unifiedJobTemplate.skip_tags !== params.node.promptValues.skip_tags ? params.node.promptValues.skip_tags : null; - } - } - - return sendableNodeData; - }; - - let continueRecursing = function(parentId) { - $scope.totalIteratedNodes++; - - if($scope.totalIteratedNodes === $scope.workflowTree.data.totalNodes) { - // We're done recursing, lets move on - completionCallback(); - } - else { - if(params.node.children && params.node.children.length > 0) { - _.forEach(params.node.children, function(child) { - if(child.edgeType === "success") { - recursiveNodeUpdates({ - parentId: parentId, - node: child - }, completionCallback); - } - else if(child.edgeType === "failure") { - recursiveNodeUpdates({ - parentId: parentId, - node: child - }, completionCallback); - } - else if(child.edgeType === "always") { - recursiveNodeUpdates({ - parentId: parentId, - node: child - }, completionCallback); - } - }); - } - } - }; - - if(params.node.isNew) { - - TemplatesService.addWorkflowNode({ - url: generatePostUrl(), - data: buildSendableNodeData() - }) - .then(function(data) { - continueRecursing(data.data.id); - }, function(error) { - ProcessErrors($scope, error.data, error.status, form, { - hdr: 'Error!', - msg: 'Failed to add workflow node. ' + - 'POST returned status: ' + - error.status - }); - }); - } - else { - if(params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) { - - if(params.node.edited) { - - $scope.editRequests.push({ - id: params.node.nodeId, - data: buildSendableNodeData() - }); - - } - - if((params.node.originalParentId && params.parentId !== params.node.originalParentId) || params.node.originalEdge !== params.node.edgeType) {//beep - - $scope.disassociateRequests.push({ - parentId: params.node.originalParentId, - nodeId: params.node.nodeId, - edge: params.node.originalEdge - }); - - // Can only associate if we have a parent. - // If we don't have a parent then this is a root node - // and the act of disassociating will make it a root node - if(params.parentId) { - $scope.associateRequests.push({ - parentId: params.parentId, - nodeId: params.node.nodeId, - edge: params.node.edgeType - }); - } - - } - else if(!params.node.originalParentId && params.parentId) { - // This used to be a root node but is now not a root node - $scope.associateRequests.push({ - parentId: params.parentId, - nodeId: params.node.nodeId, - edge: params.node.edgeType - }); - } - - } - - continueRecursing(params.node.nodeId); - } - } - $scope.openWorkflowMaker = function() { $state.go('.workflowMaker'); }; @@ -392,231 +236,97 @@ .filter("[data-label-is-present=true]") .map((i, val) => ({name: $(val).text()})); - $scope.totalIteratedNodes = 0; + TemplatesService.updateWorkflowJobTemplate({ + id: id, + data: data + }).then(function(){ - // TODO: this is the only way that I could figure out to get - // these promise arrays to play nicely. I tried to just append - // a single promise to deletePromises but it just wasn't working - let editWorkflowJobTemplate = [id].map(function(id) { - return TemplatesService.updateWorkflowJobTemplate({ - id: id, - data: data - }); - }); + var orgDefer = $q.defer(); + var associationDefer = $q.defer(); + var associatedLabelsDefer = $q.defer(); - if($scope.workflowTree && $scope.workflowTree.data && $scope.workflowTree.data.children && $scope.workflowTree.data.children.length > 0) { - let completionCallback = function() { - - let disassociatePromises = $scope.disassociateRequests.map(function(request) { - return TemplatesService.disassociateWorkflowNode({ - parentId: request.parentId, - nodeId: request.nodeId, - edge: request.edge + var getNext = function(data, arr, resolve) { + Rest.setUrl(data.next); + Rest.get() + .success(function (data) { + if (data.next) { + getNext(data, arr.concat(data.results), resolve); + } else { + resolve.resolve(arr.concat(data.results)); + } }); - }); - - let editNodePromises = $scope.editRequests.map(function(request) { - return TemplatesService.editWorkflowNode({ - id: request.id, - data: request.data - }); - }); - - $q.all(disassociatePromises.concat(editNodePromises).concat(editWorkflowJobTemplate)) - .then(function() { - - let associatePromises = $scope.associateRequests.map(function(request) { - return TemplatesService.associateWorkflowNode({ - parentId: request.parentId, - nodeId: request.nodeId, - edge: request.edge - }); - }); - - let deletePromises = $scope.workflowTree.data.deletedNodes.map(function(nodeId) { - return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); - }); - - $q.all(associatePromises.concat(deletePromises)) - .then(function() { - - var orgDefer = $q.defer(); - var associationDefer = $q.defer(); - var associatedLabelsDefer = $q.defer(); - - var getNext = function(data, arr, resolve) { - Rest.setUrl(data.next); - Rest.get() - .success(function (data) { - if (data.next) { - getNext(data, arr.concat(data.results), resolve); - } else { - resolve.resolve(arr.concat(data.results)); - } - }); - }; - - Rest.setUrl($scope.workflow_job_template_obj.related.labels); - - Rest.get() - .success(function(data) { - if (data.next) { - getNext(data, data.results, associatedLabelsDefer); - } else { - associatedLabelsDefer.resolve(data.results); - } - }); - - associatedLabelsDefer.promise.then(function (current) { - current = current.map(data => data.id); - var labelsToAdd = $scope.labels - .map(val => val.value); - var labelsToDisassociate = current - .filter(val => labelsToAdd - .indexOf(val) === -1) - .map(val => ({id: val, disassociate: true})); - var labelsToAssociate = labelsToAdd - .filter(val => current - .indexOf(val) === -1) - .map(val => ({id: val, associate: true})); - var pass = labelsToDisassociate - .concat(labelsToAssociate); - associationDefer.resolve(pass); - }); - - Rest.setUrl(GetBasePath("organizations")); - Rest.get() - .success(function(data) { - orgDefer.resolve(data.results[0].id); - }); - - orgDefer.promise.then(function(orgId) { - var toPost = []; - $scope.newLabels = $scope.newLabels - .map(function(i, val) { - val.organization = orgId; - return val; - }); - - $scope.newLabels.each(function(i, val) { - toPost.push(val); - }); - - associationDefer.promise.then(function(arr) { - toPost = toPost - .concat(arr); - - Rest.setUrl($scope.workflow_job_template_obj.related.labels); - - var defers = []; - for (var i = 0; i < toPost.length; i++) { - defers.push(Rest.post(toPost[i])); - } - $q.all(defers) - .then(function() { - $state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true}); - }); - }); - }); - - }); - }); }; - _.forEach($scope.workflowTree.data.children, function(child) { - recursiveNodeUpdates({ - node: child - }, completionCallback); - }); - } - else { + Rest.setUrl($scope.workflow_job_template_obj.related.labels); - let deletePromises = $scope.workflowTree.data.deletedNodes.map(function(nodeId) { - return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); - }); - - $q.all(deletePromises.concat(editWorkflowJobTemplate)) - .then(function() { - var orgDefer = $q.defer(); - var associationDefer = $q.defer(); - var associatedLabelsDefer = $q.defer(); - - var getNext = function(data, arr, resolve) { - Rest.setUrl(data.next); - Rest.get() - .success(function (data) { - if (data.next) { - getNext(data, arr.concat(data.results), resolve); - } else { - resolve.resolve(arr.concat(data.results)); - } - }); - }; - - Rest.setUrl($scope.workflow_job_template_obj.related.labels); - - Rest.get() - .success(function(data) { - if (data.next) { - getNext(data, data.results, associatedLabelsDefer); - } else { - associatedLabelsDefer.resolve(data.results); - } - }); - - associatedLabelsDefer.promise.then(function (current) { - current = current.map(data => data.id); - var labelsToAdd = $scope.labels - .map(val => val.value); - var labelsToDisassociate = current - .filter(val => labelsToAdd - .indexOf(val) === -1) - .map(val => ({id: val, disassociate: true})); - var labelsToAssociate = labelsToAdd - .filter(val => current - .indexOf(val) === -1) - .map(val => ({id: val, associate: true})); - var pass = labelsToDisassociate - .concat(labelsToAssociate); - associationDefer.resolve(pass); + Rest.get() + .success(function(data) { + if (data.next) { + getNext(data, data.results, associatedLabelsDefer); + } else { + associatedLabelsDefer.resolve(data.results); + } }); - Rest.setUrl(GetBasePath("organizations")); - Rest.get() - .success(function(data) { - orgDefer.resolve(data.results[0].id); + associatedLabelsDefer.promise.then(function (current) { + current = current.map(data => data.id); + var labelsToAdd = $scope.labels + .map(val => val.value); + var labelsToDisassociate = current + .filter(val => labelsToAdd + .indexOf(val) === -1) + .map(val => ({id: val, disassociate: true})); + var labelsToAssociate = labelsToAdd + .filter(val => current + .indexOf(val) === -1) + .map(val => ({id: val, associate: true})); + var pass = labelsToDisassociate + .concat(labelsToAssociate); + associationDefer.resolve(pass); + }); + + Rest.setUrl(GetBasePath("organizations")); + Rest.get() + .success(function(data) { + orgDefer.resolve(data.results[0].id); + }); + + orgDefer.promise.then(function(orgId) { + var toPost = []; + $scope.newLabels = $scope.newLabels + .map(function(i, val) { + val.organization = orgId; + return val; }); - orgDefer.promise.then(function(orgId) { - var toPost = []; - $scope.newLabels = $scope.newLabels - .map(function(i, val) { - val.organization = orgId; - return val; + $scope.newLabels.each(function(i, val) { + toPost.push(val); + }); + + associationDefer.promise.then(function(arr) { + toPost = toPost + .concat(arr); + + Rest.setUrl($scope.workflow_job_template_obj.related.labels); + + var defers = []; + for (var i = 0; i < toPost.length; i++) { + defers.push(Rest.post(toPost[i])); + } + $q.all(defers) + .then(function() { + $state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true}); }); - - $scope.newLabels.each(function(i, val) { - toPost.push(val); - }); - - associationDefer.promise.then(function(arr) { - toPost = toPost - .concat(arr); - - Rest.setUrl($scope.workflow_job_template_obj.related.labels); - - var defers = []; - for (var i = 0; i < toPost.length; i++) { - defers.push(Rest.post(toPost[i])); - } - $q.all(defers) - .then(function() { - $state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true}); - }); - }); }); }); - } + + }, function(error){ + ProcessErrors($scope, error.data, error.status, form, { + hdr: 'Error!', + msg: 'Failed to update workflow job template. PUT returned ' + + 'status: ' + error.status + }); + }); } catch (err) { Wait('stop'); diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less index 177ab6b35d..7263edbf02 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less @@ -29,6 +29,11 @@ fill: @default-interface-txt; } +.WorkflowChart-startText { + fill: @default-bg; + cursor: default; +} + .node .rect { fill: @default-secondary-bg; } @@ -97,3 +102,6 @@ width: 90px; color: @default-interface-txt; } +.WorkflowChart-activeNode { + fill: @default-link; +} diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index 3b3bc0939c..b8d8ffcbdc 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -84,6 +84,25 @@ export default [ '$state', } } + function rounded_rect(x, y, w, h, r, tl, tr, bl, br) { + var retval; + retval = "M" + (x + r) + "," + y; + retval += "h" + (w - 2*r); + if (tr) { retval += "a" + r + "," + r + " 0 0 1 " + r + "," + r; } + else { retval += "h" + r; retval += "v" + r; } + retval += "v" + (h - 2*r); + if (br) { retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + r; } + else { retval += "v" + r; retval += "h" + -r; } + retval += "h" + (2*r - w); + if (bl) { retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + -r; } + else { retval += "h" + -r; retval += "v" + -r; } + retval += "v" + (2*r - h); + if (tl) { retval += "a" + r + "," + r + " 0 0 1 " + r + "," + -r; } + else { retval += "v" + -r; retval += "h" + r; } + retval += "z"; + return retval; + } + // This is the zoom function called by using the mousewheel/click and drag function naturalZoom() { let scale = d3.event.scale, @@ -163,33 +182,46 @@ export default [ '$state', .attr("fill", "#5cb85c") .attr("class", "WorkflowChart-rootNode") .call(add_node); - thisNode.append("path") - .style("fill", "white") - .attr("transform", function() { return "translate(" + 30 + "," + 30 + ")"; }) - .attr("d", d3.svg.symbol() - .size(120) - .type("cross") - ) - .call(add_node); thisNode.append("text") - .attr("x", 14) - .attr("y", 0) + .attr("x", 13) + .attr("y", 30) .attr("dy", ".35em") - .attr("class", "WorkflowChart-defaultText") - .text(function () { return "START"; }); + .attr("class", "WorkflowChart-startText") + .text(function () { return "START"; }) + .call(add_node); } - else { + else {//d.isActiveEdit thisNode.append("rect") .attr("width", rectW) .attr("height", rectH) .attr("rx", 5) .attr("ry", 5) - .attr('stroke', function(d) { return d.isActiveEdit ? "#337ab7" : "#D7D7D7"; }) - .attr('stroke-width', function(d){ return d.isActiveEdit ? "2px" : "1px"; }) + .attr('stroke', function(d) { + if(d.edgeType) { + if(d.edgeType === "failure") { + return "#d9534f"; + } + else if(d.edgeType === "success") { + return "#5cb85c"; + } + else if(d.edgeType === "always"){ + return "#337ab7"; + } + } + else { + return "#D7D7D7"; + } + }) + .attr('stroke-width', "2px") .attr("class", function(d) { return d.placeholder ? "rect placeholder" : "rect"; }); + thisNode.append("path") + .attr("d", rounded_rect(1, 0, 5, rectH, 5, 1, 0, 1, 0)) + .attr("class", "WorkflowChart-activeNode") + .style("display", function(d) { return d.isActiveEdit ? null : "none"; }); + thisNode.append("text") .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 20 : rectW / 2; }) .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 10 : rectH / 2; }) @@ -517,8 +549,22 @@ export default [ '$state', .attr("transform", function(d) { return "translate(" + (d.target.y + d.source.y + rectW) / 2 + "," + (d.target.x + d.source.x + rectH) / 2 + ")"; }); t.selectAll(".rect") - .attr('stroke', function(d) { return d.isActiveEdit ? "#337ab7" : "#D7D7D7"; }) - .attr('stroke-width', function(d){ return d.isActiveEdit ? "2px" : "1px"; }) + .attr('stroke', function(d) { + if(d.edgeType) { + if(d.edgeType === "failure") { + return "#d9534f"; + } + else if(d.edgeType === "success") { + return "#5cb85c"; + } + else if(d.edgeType === "always"){ + return "#337ab7"; + } + } + else { + return "#D7D7D7"; + } + }) .attr("class", function(d) { return d.placeholder ? "rect placeholder" : "rect"; }); @@ -601,6 +647,9 @@ export default [ '$state', t.selectAll(".WorkflowChart-conflictText") .style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; }); + t.selectAll(".WorkflowChart-activeNode") + .style("display", function(d) { return d.isActiveEdit ? null : "none"; }); + } function add_node() { diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less index 04ec4dd51a..4fd54ad08f 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less @@ -154,7 +154,7 @@ padding-left: 20px; } .WorkflowLegend-maker--right { - flex: 0 0 182px; + flex: 0 0 206px; text-align: right; padding-right: 20px; position: relative; @@ -226,7 +226,7 @@ } .WorkflowMaker-manualControls { position: absolute; - left: -122px; + left: -86px; height: 60px; width: 293px; background-color: @default-bg; diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js index ac2fe30396..1e4f923a37 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js @@ -35,6 +35,10 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr showTypeOptions: false }; + $scope.editRequests = []; + $scope.associateRequests = []; + $scope.disassociateRequests = []; + function init() { $scope.treeDataMaster = angular.copy($scope.treeData.data); $scope.showManualControls = false; @@ -55,6 +59,160 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr $scope.workflowMakerFormConfig.activeTab = "jobs"; } + function recursiveNodeUpdates(params, completionCallback) { + // params.parentId + // params.node + + let generatePostUrl = function(){ + + let base = (params.parentId) ? GetBasePath('workflow_job_template_nodes') + params.parentId : $scope.treeData.workflow_job_template_obj.related.workflow_nodes; + + if(params.parentId) { + if(params.node.edgeType === 'success') { + base += "/success_nodes"; + } + else if(params.node.edgeType === 'failure') { + base += "/failure_nodes"; + } + else if(params.node.edgeType === 'always') { + base += "/always_nodes"; + } + } + + return base; + + }; + + let buildSendableNodeData = function() { + // Create the node + let sendableNodeData = { + unified_job_template: params.node.unifiedJobTemplate.id + }; + + // Check to see if the user has provided any prompt values that are different + // from the defaults in the job template + + if(params.node.unifiedJobTemplate.type === "job_template" && params.node.promptValues) { + if(params.node.unifiedJobTemplate.ask_credential_on_launch) { + sendableNodeData.credential = !params.node.promptValues.credential || params.node.unifiedJobTemplate.summary_fields.credential.id !== params.node.promptValues.credential.id ? params.node.promptValues.credential.id : null; + } + if(params.node.unifiedJobTemplate.ask_inventory_on_launch) { + sendableNodeData.inventory = !params.node.promptValues.inventory || params.node.unifiedJobTemplate.summary_fields.inventory.id !== params.node.promptValues.inventory.id ? params.node.promptValues.inventory.id : null; + } + if(params.node.unifiedJobTemplate.ask_limit_on_launch) { + sendableNodeData.limit = !params.node.promptValues.limit || params.node.unifiedJobTemplate.limit !== params.node.promptValues.limit ? params.node.promptValues.limit : null; + } + if(params.node.unifiedJobTemplate.ask_job_type_on_launch) { + sendableNodeData.job_type = !params.node.promptValues.job_type || params.node.unifiedJobTemplate.job_type !== params.node.promptValues.job_type ? params.node.promptValues.job_type : null; + } + if(params.node.unifiedJobTemplate.ask_tags_on_launch) { + sendableNodeData.job_tags = !params.node.promptValues.job_tags || params.node.unifiedJobTemplate.job_tags !== params.node.promptValues.job_tags ? params.node.promptValues.job_tags : null; + } + if(params.node.unifiedJobTemplate.ask_skip_tags_on_launch) { + sendableNodeData.skip_tags = !params.node.promptValues.skip_tags || params.node.unifiedJobTemplate.skip_tags !== params.node.promptValues.skip_tags ? params.node.promptValues.skip_tags : null; + } + } + + return sendableNodeData; + }; + + let continueRecursing = function(parentId) { + $scope.totalIteratedNodes++; + + if($scope.totalIteratedNodes === $scope.treeData.data.totalNodes) { + // We're done recursing, lets move on + completionCallback(); + } + else { + if(params.node.children && params.node.children.length > 0) { + _.forEach(params.node.children, function(child) { + if(child.edgeType === "success") { + recursiveNodeUpdates({ + parentId: parentId, + node: child + }, completionCallback); + } + else if(child.edgeType === "failure") { + recursiveNodeUpdates({ + parentId: parentId, + node: child + }, completionCallback); + } + else if(child.edgeType === "always") { + recursiveNodeUpdates({ + parentId: parentId, + node: child + }, completionCallback); + } + }); + } + } + }; + + if(params.node.isNew) { + + TemplatesService.addWorkflowNode({ + url: generatePostUrl(), + data: buildSendableNodeData() + }) + .then(function(data) { + continueRecursing(data.data.id); + }, function(error) { + ProcessErrors($scope, error.data, error.status, form, { + hdr: 'Error!', + msg: 'Failed to add workflow node. ' + + 'POST returned status: ' + + error.status + }); + }); + } + else { + if(params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) { + + if(params.node.edited) { + + $scope.editRequests.push({ + id: params.node.nodeId, + data: buildSendableNodeData() + }); + + } + + if((params.node.originalParentId && params.parentId !== params.node.originalParentId) || params.node.originalEdge !== params.node.edgeType) {//beep + + $scope.disassociateRequests.push({ + parentId: params.node.originalParentId, + nodeId: params.node.nodeId, + edge: params.node.originalEdge + }); + + // Can only associate if we have a parent. + // If we don't have a parent then this is a root node + // and the act of disassociating will make it a root node + if(params.parentId) { + $scope.associateRequests.push({ + parentId: params.parentId, + nodeId: params.node.nodeId, + edge: params.node.edgeType + }); + } + + } + else if(!params.node.originalParentId && params.parentId) { + // This used to be a root node but is now not a root node + $scope.associateRequests.push({ + parentId: params.parentId, + nodeId: params.node.nodeId, + edge: params.node.edgeType + }); + } + + } + + continueRecursing(params.node.nodeId); + } + } + $scope.lookUpInventory = function(){ $state.go('.inventory'); }; @@ -70,7 +228,66 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr }; $scope.saveWorkflowMaker = function() { - $scope.closeDialog(); + + $scope.totalIteratedNodes = 0; + + if($scope.treeData && $scope.treeData.data && $scope.treeData.data.children && $scope.treeData.data.children.length > 0) { + let completionCallback = function() { + + let disassociatePromises = $scope.disassociateRequests.map(function(request) { + return TemplatesService.disassociateWorkflowNode({ + parentId: request.parentId, + nodeId: request.nodeId, + edge: request.edge + }); + }); + + let editNodePromises = $scope.editRequests.map(function(request) { + return TemplatesService.editWorkflowNode({ + id: request.id, + data: request.data + }); + }); + + $q.all(disassociatePromises.concat(editNodePromises)) + .then(function() { + + let associatePromises = $scope.associateRequests.map(function(request) { + return TemplatesService.associateWorkflowNode({ + parentId: request.parentId, + nodeId: request.nodeId, + edge: request.edge + }); + }); + + let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) { + return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); + }); + + $q.all(associatePromises.concat(deletePromises)) + .then(function() { + $scope.closeDialog(); + }); + }); + }; + + _.forEach($scope.treeData.data.children, function(child) { + recursiveNodeUpdates({ + node: child + }, completionCallback); + }); + } + else { + + let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) { + return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); + }); + + $q.all(deletePromises) + .then(function() { + $scope.closeDialog(); + }); + } }; /* ADD NODE FUNCTIONS */ @@ -575,7 +792,7 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr edgeFlags: $scope.edgeFlags }); } - + $scope.toggleManualControls = function() { $scope.showManualControls = !$scope.showManualControls; }; diff --git a/awx/ui/tests/spec/workflows/workflow-maker.controller-test.js b/awx/ui/tests/spec/workflows/workflow-maker.controller-test.js index 12bbf8ef74..6adc771b27 100644 --- a/awx/ui/tests/spec/workflows/workflow-maker.controller-test.js +++ b/awx/ui/tests/spec/workflows/workflow-maker.controller-test.js @@ -47,10 +47,10 @@ describe('Controller: WorkflowMaker', () => { })); - describe('scope.saveWorkflowMaker()', () => { + describe('scope.closeWorkflowMaker()', () => { it('should close the dialog', ()=>{ - scope.saveWorkflowMaker(); + scope.closeWorkflowMaker(); expect(scope.closeDialog).toHaveBeenCalled(); }); From 278d70ed49bcffd0b183d527fdbd869a7128d811 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 12 Dec 2016 11:21:21 -0500 Subject: [PATCH 082/595] do not copy WFJT labels in 3.1 --- awx/main/models/workflow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 1cd4d44348..2a0b25f718 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -366,7 +366,9 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl @classmethod def _get_unified_jt_copy_names(cls): - return (super(WorkflowJobTemplate, cls)._get_unified_jt_copy_names() + + base_list = super(WorkflowJobTemplate, cls)._get_unified_jt_copy_names() + base_list.remove('labels') + return (base_list + ['survey_spec', 'survey_enabled', 'organization']) def get_absolute_url(self): From fdcae226aacbcd53261e4eff6b1cb1ba9d3c646e Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 12 Dec 2016 11:25:20 -0500 Subject: [PATCH 083/595] Removed rogue comment - not needed --- .../workflows/workflow-chart/workflow-chart.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index b8d8ffcbdc..cb6eeafa28 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -190,7 +190,7 @@ export default [ '$state', .text(function () { return "START"; }) .call(add_node); } - else {//d.isActiveEdit + else { thisNode.append("rect") .attr("width", rectW) .attr("height", rectH) From 933cc83b90bffba0f065539b284e44caee48bbe0 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Mon, 12 Dec 2016 11:35:41 -0500 Subject: [PATCH 084/595] Mark up UsersAdd properly. --- awx/ui/client/src/controllers/Users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/controllers/Users.js b/awx/ui/client/src/controllers/Users.js index cf7f63b9a1..9aade4fbf8 100644 --- a/awx/ui/client/src/controllers/Users.js +++ b/awx/ui/client/src/controllers/Users.js @@ -201,7 +201,7 @@ export function UsersAdd($scope, $rootScope, $stateParams, UserForm, UsersAdd.$inject = ['$scope', '$rootScope', '$stateParams', 'UserForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'GetBasePath', - 'ResetForm', 'Wait', 'CreateSelect2', '$state', '$location' + 'ResetForm', 'Wait', 'CreateSelect2', '$state', '$location', 'i18n' ]; export function UsersEdit($scope, $rootScope, $location, From 08edbb1b723880ac81b63e5d1ca31c331f79b0a8 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 12 Dec 2016 12:01:48 -0500 Subject: [PATCH 085/595] Set an upper limit of 200 on the max page size --- awx/api/pagination.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/api/pagination.py b/awx/api/pagination.py index ee17aee0e1..b0ccbeaf01 100644 --- a/awx/api/pagination.py +++ b/awx/api/pagination.py @@ -9,6 +9,7 @@ from rest_framework.utils.urls import replace_query_param class Pagination(pagination.PageNumberPagination): page_size_query_param = 'page_size' + max_page_size = 200 def get_next_link(self): if not self.page.has_next(): From 249469c57613fa49a6a2206976d56507b4742b27 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 12 Dec 2016 12:02:56 -0500 Subject: [PATCH 086/595] encrypt log token or password --- awx/main/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/conf.py b/awx/main/conf.py index 229e8aaf93..00d81ed356 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -265,6 +265,7 @@ register( 'LOG_AGGREGATOR_PASSWORD', field_class=fields.CharField, allow_null=True, + encrypted=True, label=_('Logging Aggregator Password to Authenticate With'), help_text=_('Password for Logstash or others (basic auth)'), category=_('Logging'), From 349d497bb4ff94a05c63b3edbad00f036b8a6041 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 12 Dec 2016 12:10:33 -0500 Subject: [PATCH 087/595] Make max page size tunable --- awx/api/pagination.py | 3 ++- awx/settings/defaults.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/awx/api/pagination.py b/awx/api/pagination.py index b0ccbeaf01..b6463ce515 100644 --- a/awx/api/pagination.py +++ b/awx/api/pagination.py @@ -2,6 +2,7 @@ # All Rights Reserved. # Django REST Framework +from django.conf import settings from rest_framework import pagination from rest_framework.utils.urls import replace_query_param @@ -9,7 +10,7 @@ from rest_framework.utils.urls import replace_query_param class Pagination(pagination.PageNumberPagination): page_size_query_param = 'page_size' - max_page_size = 200 + max_page_size = settings.MAX_PAGE_SIZE def get_next_link(self): if not self.page.has_next(): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index b9fa948521..4a15abef7b 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -223,6 +223,7 @@ INSTALLED_APPS = ( INTERNAL_IPS = ('127.0.0.1',) +MAX_PAGE_SIZE = 200 REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'awx.api.pagination.Pagination', 'PAGE_SIZE': 25, From bb7a201c2fcb66c61b3b0cce473ff28b14c151d3 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 12 Dec 2016 12:24:40 -0500 Subject: [PATCH 088/595] Add missing field to ad-hoc command partial --- awx/main/scheduler/partial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/scheduler/partial.py b/awx/main/scheduler/partial.py index 50c8d6653b..b5c449cd17 100644 --- a/awx/main/scheduler/partial.py +++ b/awx/main/scheduler/partial.py @@ -251,7 +251,7 @@ class SystemJobDict(PartialModelDict): class AdHocCommandDict(PartialModelDict): FIELDS = ( - 'id', 'created', 'status', 'inventory_id', 'dependent_jobs__id', + 'id', 'created', 'status', 'inventory_id', 'dependent_jobs__id', 'celery_task_id', ) model = AdHocCommand From 8b8c29b9dcc7b0bd3ce700174a4d1bd3744fdcb0 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 12 Dec 2016 12:32:49 -0500 Subject: [PATCH 089/595] Purge has_schedules Also... migration for big field docs updates --- awx/api/serializers.py | 2 +- .../migrations/0054_text_and_has_schedules.py | 133 ++++++++++++++++++ awx/main/models/unified_jobs.py | 4 - 3 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 awx/main/migrations/0054_text_and_has_schedules.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index b5a5b326ef..11d3c10234 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -519,7 +519,7 @@ class UnifiedJobTemplateSerializer(BaseSerializer): class Meta: model = UnifiedJobTemplate - fields = ('*', 'last_job_run', 'last_job_failed', 'has_schedules', + fields = ('*', 'last_job_run', 'last_job_failed', 'next_job_run', 'status') def get_related(self, obj): diff --git a/awx/main/migrations/0054_text_and_has_schedules.py b/awx/main/migrations/0054_text_and_has_schedules.py new file mode 100644 index 0000000000..426e40f211 --- /dev/null +++ b/awx/main/migrations/0054_text_and_has_schedules.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0053_v310_update_timeout_field_type'), + ] + + operations = [ + migrations.RemoveField( + model_name='unifiedjobtemplate', + name='has_schedules', + ), + migrations.AlterField( + model_name='host', + name='instance_id', + field=models.CharField(default=b'', help_text='The value used by the remote inventory source to uniquely identify the host', max_length=1024, blank=True), + ), + migrations.AlterField( + model_name='project', + name='scm_clean', + field=models.BooleanField(default=False, help_text='Discard any local changes before syncing the project.'), + ), + migrations.AlterField( + model_name='project', + name='scm_delete_on_update', + field=models.BooleanField(default=False, help_text='Delete the project before syncing.'), + ), + migrations.AlterField( + model_name='project', + name='scm_type', + field=models.CharField(default=b'', choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion')], max_length=8, blank=True, help_text='Specifies the source control system used to store the project.', verbose_name='SCM Type'), + ), + migrations.AlterField( + model_name='project', + name='scm_update_cache_timeout', + field=models.PositiveIntegerField(default=0, help_text='The number of seconds after the last project update ran that a newproject update will be launched as a job dependency.', blank=True), + ), + migrations.AlterField( + model_name='project', + name='scm_update_on_launch', + field=models.BooleanField(default=False, help_text='Update the project when a job is launched that uses the project.'), + ), + migrations.AlterField( + model_name='project', + name='scm_url', + field=models.CharField(default=b'', help_text='The location where the project is stored.', max_length=1024, verbose_name='SCM URL', blank=True), + ), + migrations.AlterField( + model_name='project', + name='timeout', + field=models.IntegerField(default=0, help_text='The amount of time to run before the task is canceled.', blank=True), + ), + migrations.AlterField( + model_name='projectupdate', + name='scm_clean', + field=models.BooleanField(default=False, help_text='Discard any local changes before syncing the project.'), + ), + migrations.AlterField( + model_name='projectupdate', + name='scm_delete_on_update', + field=models.BooleanField(default=False, help_text='Delete the project before syncing.'), + ), + migrations.AlterField( + model_name='projectupdate', + name='scm_type', + field=models.CharField(default=b'', choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion')], max_length=8, blank=True, help_text='Specifies the source control system used to store the project.', verbose_name='SCM Type'), + ), + migrations.AlterField( + model_name='projectupdate', + name='scm_url', + field=models.CharField(default=b'', help_text='The location where the project is stored.', max_length=1024, verbose_name='SCM URL', blank=True), + ), + migrations.AlterField( + model_name='projectupdate', + name='timeout', + field=models.IntegerField(default=0, help_text='The amount of time to run before the task is canceled.', blank=True), + ), + migrations.AlterField( + model_name='schedule', + name='dtend', + field=models.DateTimeField(default=None, help_text='The last occurrence of the schedule occurs before this time, aftewards the schedule expires.', null=True, editable=False), + ), + migrations.AlterField( + model_name='schedule', + name='dtstart', + field=models.DateTimeField(default=None, help_text='The first occurrence of the schedule occurs on or after this time.', null=True, editable=False), + ), + migrations.AlterField( + model_name='schedule', + name='enabled', + field=models.BooleanField(default=True, help_text='Enables processing of this schedule by Tower.'), + ), + migrations.AlterField( + model_name='schedule', + name='next_run', + field=models.DateTimeField(default=None, help_text='The next time that the scheduled action will run.', null=True, editable=False), + ), + migrations.AlterField( + model_name='schedule', + name='rrule', + field=models.CharField(help_text='A value representing the schedules iCal recurrence rule.', max_length=255), + ), + migrations.AlterField( + model_name='unifiedjob', + name='elapsed', + field=models.DecimalField(help_text='Elapsed time in seconds that the job ran.', editable=False, max_digits=12, decimal_places=3), + ), + migrations.AlterField( + model_name='unifiedjob', + name='execution_node', + field=models.TextField(default=b'', help_text='The Tower node the job executed on.', editable=False, blank=True), + ), + migrations.AlterField( + model_name='unifiedjob', + name='finished', + field=models.DateTimeField(default=None, help_text='The date and time the job finished execution.', null=True, editable=False), + ), + migrations.AlterField( + model_name='unifiedjob', + name='job_explanation', + field=models.TextField(default=b'', help_text="A status field to indicate the state of the job if it wasn't able to run and capture stdout", editable=False, blank=True), + ), + migrations.AlterField( + model_name='unifiedjob', + name='started', + field=models.DateTimeField(default=None, help_text='The date and time the job was queued for starting.', null=True, editable=False), + ), + ] diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 1a340345ea..bda60b0c1d 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -122,10 +122,6 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio default=None, editable=False, ) - has_schedules = models.BooleanField( - default=False, - editable=False, - ) #on_missed_schedule = models.CharField( # max_length=32, # choices=[], From 4c8af2a4b8c9a95897ae097a2ba173e59e79d798 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 9 Dec 2016 10:28:38 -0500 Subject: [PATCH 090/595] update uwsgi/nginx dev configuration --- tools/docker-compose/nginx.conf | 6 ----- tools/docker-compose/nginx.vh.default.conf | 27 ++++++++++++++++++---- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/tools/docker-compose/nginx.conf b/tools/docker-compose/nginx.conf index 4f1f4e986e..9c9b7510e1 100644 --- a/tools/docker-compose/nginx.conf +++ b/tools/docker-compose/nginx.conf @@ -25,12 +25,6 @@ http { sendfile on; #tcp_nopush on; - - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - - keepalive_timeout 65; - #gzip on; include /etc/nginx/conf.d/*.conf; diff --git a/tools/docker-compose/nginx.vh.default.conf b/tools/docker-compose/nginx.vh.default.conf index 2325057378..bda25f75d2 100644 --- a/tools/docker-compose/nginx.vh.default.conf +++ b/tools/docker-compose/nginx.vh.default.conf @@ -7,17 +7,32 @@ upstream daphne { } server { - listen 8013 default_server; + listen 8013 default_server; + listen [::]:8013 default_server; + return 301 https://$host:8043$request_uri; +} + +server { listen 8043 default_server ssl; # If you have a domain name, this is where to add it server_name _; - keepalive_timeout 70; + keepalive_timeout 60; ssl_certificate /etc/nginx/nginx.crt; ssl_certificate_key /etc/nginx/nginx.key; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers HIGH:!aNULL:!MD5; + + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:50m; + ssl_session_tickets off; + + # intermediate configuration. tweak to your needs. + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; + ssl_prefer_server_ciphers on; + + # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) + add_header Strict-Transport-Security max-age=15768000; location /static/ { root /tower_devel; @@ -49,7 +64,9 @@ server { } location / { + uwsgi_read_timeout 30s; + uwsgi_send_timeout 30s; + uwsgi_pass uwsgi; include /etc/nginx/uwsgi_params; - uwsgi_pass uwsgi; } } From 88dc742f423968ebb6ca93ccfb3abc20b85feafd Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 9 Dec 2016 10:29:03 -0500 Subject: [PATCH 091/595] update uwsgi/nginx production configuration --- config/awx-nginx.conf | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/config/awx-nginx.conf b/config/awx-nginx.conf index 3df3155ec8..a14dd036cf 100644 --- a/config/awx-nginx.conf +++ b/config/awx-nginx.conf @@ -24,12 +24,6 @@ http { sendfile on; #tcp_nopush on; - - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - - keepalive_timeout 65; - #gzip on; upstream uwsgi { @@ -42,16 +36,30 @@ http { server { listen 80 default_server; + listen [::]:80 default_server; + return 301 https://$host$request_uri; + } + + server { listen 443 default_server ssl; # If you have a domain name, this is where to add it server_name _; - keepalive_timeout 70; + keepalive_timeout 60; ssl_certificate /etc/tower/tower.cert; ssl_certificate_key /etc/tower/tower.key; + ssl_session_cache shared:SSL:50m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + # intermediate configuration ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers HIGH:!aNULL:!MD5; + ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; + ssl_prefer_server_ciphers on; + + # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) + add_header Strict-Transport-Security max-age=15768000; location /favicon.ico { alias /var/lib/awx/public/static/favicon.ico; } location /static { alias /var/lib/awx/public/static; } @@ -79,8 +87,10 @@ http { } location / { - include /etc/nginx/uwsgi_params; + uwsgi_read_timeout 30s; + uwsgi_send_timeout 30s; uwsgi_pass uwsgi; + include /etc/nginx/uwsgi_params; } } } From 5f5624c5a3dd69fef4e451cf504c8ae5a5b3942f Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 9 Dec 2016 11:00:52 -0500 Subject: [PATCH 092/595] Update to use Modern values for SSL --- tools/docker-compose/nginx.vh.default.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/docker-compose/nginx.vh.default.conf b/tools/docker-compose/nginx.vh.default.conf index bda25f75d2..98c704671c 100644 --- a/tools/docker-compose/nginx.vh.default.conf +++ b/tools/docker-compose/nginx.vh.default.conf @@ -27,8 +27,8 @@ server { ssl_session_tickets off; # intermediate configuration. tweak to your needs. - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; + ssl_protocols TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; ssl_prefer_server_ciphers on; # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) From fafec3a0e3359cafce2631389179fb4e9ed9407c Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 9 Dec 2016 11:01:48 -0500 Subject: [PATCH 093/595] Update to use Modern values for SSL --- config/awx-nginx.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/awx-nginx.conf b/config/awx-nginx.conf index a14dd036cf..eefb763834 100644 --- a/config/awx-nginx.conf +++ b/config/awx-nginx.conf @@ -54,8 +54,8 @@ http { ssl_session_tickets off; # intermediate configuration - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; + ssl_protocols TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; ssl_prefer_server_ciphers on; # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) From c6e1598b9cf6c376106dbb1b32eef1e05862ad4d Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 12 Dec 2016 00:58:06 -0500 Subject: [PATCH 094/595] explicitly set long harakiri time to deal with very large inventory deletes --- Makefile | 2 +- config/awx-nginx.conf | 9 ++++----- tools/docker-compose/nginx.vh.default.conf | 7 +++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 8bf3766abd..27aa0419f2 100644 --- a/Makefile +++ b/Makefile @@ -406,7 +406,7 @@ uwsgi: collectstatic @if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/tower/bin/activate; \ fi; \ - uwsgi -b 32768 --socket :8050 --module=awx.wsgi:application --home=/venv/tower --chdir=/tower_devel/ --vacuum --processes=5 --harakiri=60 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --master-fifo=/tmp/awxfifo --lazy-apps + uwsgi -b 32768 --socket :8050 --module=awx.wsgi:application --home=/venv/tower --chdir=/tower_devel/ --vacuum --processes=5 --harakiri=120 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --master-fifo=/tmp/awxfifo --lazy-apps daphne: @if [ "$(VENV_BASE)" ]; then \ diff --git a/config/awx-nginx.conf b/config/awx-nginx.conf index eefb763834..30806cbfe8 100644 --- a/config/awx-nginx.conf +++ b/config/awx-nginx.conf @@ -23,8 +23,8 @@ http { } sendfile on; - #tcp_nopush on; - #gzip on; + tcp_nopush on; + tcp_nodelay on; upstream uwsgi { server 127.0.0.1:8050; @@ -45,7 +45,7 @@ http { # If you have a domain name, this is where to add it server_name _; - keepalive_timeout 60; + keepalive_timeout 65; ssl_certificate /etc/tower/tower.cert; ssl_certificate_key /etc/tower/tower.key; @@ -87,8 +87,7 @@ http { } location / { - uwsgi_read_timeout 30s; - uwsgi_send_timeout 30s; + uwsgi_read_timeout 120s; uwsgi_pass uwsgi; include /etc/nginx/uwsgi_params; } diff --git a/tools/docker-compose/nginx.vh.default.conf b/tools/docker-compose/nginx.vh.default.conf index 98c704671c..456b16953d 100644 --- a/tools/docker-compose/nginx.vh.default.conf +++ b/tools/docker-compose/nginx.vh.default.conf @@ -17,7 +17,7 @@ server { # If you have a domain name, this is where to add it server_name _; - keepalive_timeout 60; + keepalive_timeout 65; ssl_certificate /etc/nginx/nginx.crt; ssl_certificate_key /etc/nginx/nginx.key; @@ -64,9 +64,8 @@ server { } location / { - uwsgi_read_timeout 30s; - uwsgi_send_timeout 30s; - uwsgi_pass uwsgi; + uwsgi_read_timeout 120s; + uwsgi_pass uwsgi; include /etc/nginx/uwsgi_params; } } From 19f7d9e8d8b8830591096cf2083c6adbcfc8fa22 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 12 Dec 2016 13:06:42 -0500 Subject: [PATCH 095/595] redirect if no forward-slash --- config/awx-nginx.conf | 2 + tools/docker-compose/nginx.vh.default.conf | 66 +++++++++++----------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/config/awx-nginx.conf b/config/awx-nginx.conf index 30806cbfe8..15e58678ba 100644 --- a/config/awx-nginx.conf +++ b/config/awx-nginx.conf @@ -87,6 +87,8 @@ http { } location / { + # Redirect if there is no forward-slash + rewrite ^(.*[^/])$ $1/ permanent; uwsgi_read_timeout 120s; uwsgi_pass uwsgi; include /etc/nginx/uwsgi_params; diff --git a/tools/docker-compose/nginx.vh.default.conf b/tools/docker-compose/nginx.vh.default.conf index 456b16953d..5cabf89bbb 100644 --- a/tools/docker-compose/nginx.vh.default.conf +++ b/tools/docker-compose/nginx.vh.default.conf @@ -7,20 +7,20 @@ upstream daphne { } server { - listen 8013 default_server; - listen [::]:8013 default_server; - return 301 https://$host:8043$request_uri; + listen 8013 default_server; + listen [::]:8013 default_server; + return 301 https://$host:8043$request_uri; } server { - listen 8043 default_server ssl; + listen 8043 default_server ssl; - # If you have a domain name, this is where to add it + # If you have a domain name, this is where to add it server_name _; - keepalive_timeout 65; + keepalive_timeout 65; - ssl_certificate /etc/nginx/nginx.crt; - ssl_certificate_key /etc/nginx/nginx.key; + ssl_certificate /etc/nginx/nginx.crt; + ssl_certificate_key /etc/nginx/nginx.key; ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; @@ -41,31 +41,33 @@ server { sendfile off; } - location /websocket { - # Pass request to the upstream alias - proxy_pass http://daphne; - # Require http version 1.1 to allow for upgrade requests - proxy_http_version 1.1; - # We want proxy_buffering off for proxying to websockets. - proxy_buffering off; - # http://en.wikipedia.org/wiki/X-Forwarded-For - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - # enable this if you use HTTPS: - proxy_set_header X-Forwarded-Proto https; - # pass the Host: header from the client for the sake of redirects - proxy_set_header Host $http_host; - # We've set the Host header, so we don't need Nginx to muddle - # about with redirects - proxy_redirect off; - # Depending on the request value, set the Upgrade and - # connection headers - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } + location /websocket { + # Pass request to the upstream alias + proxy_pass http://daphne; + # Require http version 1.1 to allow for upgrade requests + proxy_http_version 1.1; + # We want proxy_buffering off for proxying to websockets. + proxy_buffering off; + # http://en.wikipedia.org/wiki/X-Forwarded-For + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # enable this if you use HTTPS: + proxy_set_header X-Forwarded-Proto https; + # pass the Host: header from the client for the sake of redirects + proxy_set_header Host $http_host; + # We've set the Host header, so we don't need Nginx to muddle + # about with redirects + proxy_redirect off; + # Depending on the request value, set the Upgrade and + # connection headers + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } - location / { + location / { + # Add trailing / if missing + rewrite ^(.*[^/])$ $1/ permanent; uwsgi_read_timeout 120s; uwsgi_pass uwsgi; - include /etc/nginx/uwsgi_params; - } + include /etc/nginx/uwsgi_params; + } } From deca2bb5c1eef583dee16cce285996b05d42b32b Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 12 Dec 2016 14:02:59 -0500 Subject: [PATCH 096/595] Various fixes to the templates list based on audit feedback --- awx/ui/client/src/lists/Templates.js | 6 +++-- .../list-generator/list-actions.partial.html | 4 +-- .../list-generator/list-generator.factory.js | 25 +++++++++++++++++-- .../templates/labels/labelsList.directive.js | 6 ++--- .../templates/labels/labelsList.partial.html | 6 ++--- .../list/templates-list.controller.js | 2 +- 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/awx/ui/client/src/lists/Templates.js b/awx/ui/client/src/lists/Templates.js index 7db934ffd3..67728dada0 100644 --- a/awx/ui/client/src/lists/Templates.js +++ b/awx/ui/client/src/lists/Templates.js @@ -47,6 +47,7 @@ export default label: i18n._('Labels'), type: 'labels', nosort: true, + showDelete: true, columnClass: 'List-tableCell col-lg-2 col-md-4 hidden-sm hidden-xs' } }, @@ -58,7 +59,7 @@ export default basePaths: ['templates'], awToolTip: i18n._('Create a new template'), actionClass: 'btn List-dropdownSuccess', - buttonContent: i18n._('ADD'), + buttonContent: '+ ' + i18n._('ADD'), options: [ { optionContent: i18n._('Job Template'), @@ -109,7 +110,8 @@ export default awToolTip: i18n._('Edit template'), "class": 'btn-default btn-xs', dataPlacement: 'top', - ngShow: 'template.summary_fields.user_capabilities.edit' + ngShow: 'template.summary_fields.user_capabilities.edit', + editStateParams: ['job_template_id', 'workflow_job_template_id'] }, view: { label: i18n._('View'), diff --git a/awx/ui/client/src/shared/list-generator/list-actions.partial.html b/awx/ui/client/src/shared/list-generator/list-actions.partial.html index 796b370924..026852ef3a 100644 --- a/awx/ui/client/src/shared/list-generator/list-actions.partial.html +++ b/awx/ui/client/src/shared/list-generator/list-actions.partial.html @@ -29,9 +29,9 @@ -
+
diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 9f66266bc0..d9b5702f60 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -355,6 +355,16 @@ export default ['$location', '$compile', '$rootScope', 'Attr', 'Icon', innerTable += "
"; + let handleEditStateParams = function(stateParams){console.log(stateParams); + let matchingConditions = []; + + angular.forEach(stateParams, function(stateParam) { + matchingConditions.push(`$stateParams['` + stateParam + `'] == ${list.iterator}.id`); + }); + + return matchingConditions; + }; + for (field_action in list.fieldActions) { if (field_action !== 'columnClass') { if (list.fieldActions[field_action].type && list.fieldActions[field_action].type === 'DropDown') { @@ -376,8 +386,19 @@ export default ['$location', '$compile', '$rootScope', 'Attr', 'Icon', innerTable += "class=\"List-actionButton "; innerTable += (field_action === 'delete' || field_action === 'cancel') ? "List-actionButton--delete" : ""; innerTable += "\" "; - // rowBeingEdited === '{{ " + list.iterator + ".id }}' && listBeingEdited === '" + list.name + "' ? 'List-tableRow--selected' : ''"; - innerTable += (field_action === 'edit') ? `ng-class="{'List-editButton--selected' : $stateParams['${list.iterator}_id'] == ${list.iterator}.id}"`: ''; + if(field_action === 'edit') { + // editStateParams allows us to handle cases where a list might have different types of resources in it. As a result the edit + // icon might now always point to the same state and differing states may have differing stateParams. Specifically this occurs + // on the Templates list where editing a workflow job template takes you to a state where the param is workflow_job_template_id. + // You can also edit a Job Template from this list so the stateParam there would be job_template_id. + if(list.fieldActions[field_action].editStateParams) { + let matchingConditions = handleEditStateParams(list.fieldActions[field_action].editStateParams); + innerTable += `ng-class="{'List-editButton--selected' : ${matchingConditions.join(' || ')}}"`; + } + else { + innerTable += `ng-class="{'List-editButton--selected' : $stateParams['${list.iterator}_id'] == ${list.iterator}.id}"`; + } + } innerTable += (fAction.awPopOver) ? "aw-pop-over=\"" + fAction.awPopOver + "\" " : ""; innerTable += (fAction.dataPlacement) ? Attr(fAction, 'dataPlacement') : ""; innerTable += (fAction.dataTitle) ? Attr(fAction, 'dataTitle') : ""; diff --git a/awx/ui/client/src/templates/labels/labelsList.directive.js b/awx/ui/client/src/templates/labels/labelsList.directive.js index 54c49ef47c..c7b83618db 100644 --- a/awx/ui/client/src/templates/labels/labelsList.directive.js +++ b/awx/ui/client/src/templates/labels/labelsList.directive.js @@ -8,7 +8,8 @@ export default 'Prompt', '$q', '$filter', - function(templateUrl, Wait, Rest, GetBasePath, ProcessErrors, Prompt, $q, $filter) { + '$state', + function(templateUrl, Wait, Rest, GetBasePath, ProcessErrors, Prompt, $q, $filter, $state) { return { restrict: 'E', scope: false, @@ -63,9 +64,8 @@ export default Rest.setUrl(url); Rest.post({"disassociate": true, "id": labelId}) .success(function () { - // @issue: OLD SEARCH - // scope.search("job_template", scope.$parent.job_template_page); Wait('stop'); + $state.go('.', null, {reload: true}); }) .error(function (data, status) { Wait('stop'); diff --git a/awx/ui/client/src/templates/labels/labelsList.partial.html b/awx/ui/client/src/templates/labels/labelsList.partial.html index b14ec6deb9..0837cdfc00 100644 --- a/awx/ui/client/src/templates/labels/labelsList.partial.html +++ b/awx/ui/client/src/templates/labels/labelsList.partial.html @@ -1,10 +1,10 @@
+ ng-click="deleteLabel(template.id, template.name, label.id, label.name)" + ng-show="showDelete && template.summary_fields.user_capabilities.edit">
-
+
{{ label.name }}
diff --git a/awx/ui/client/src/templates/list/templates-list.controller.js b/awx/ui/client/src/templates/list/templates-list.controller.js index f984dd57e3..93f32807ea 100644 --- a/awx/ui/client/src/templates/list/templates-list.controller.js +++ b/awx/ui/client/src/templates/list/templates-list.controller.js @@ -107,7 +107,7 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', 'Al if(template) { Prompt({ hdr: 'Delete', - body: '
Are you sure you want to delete the ' + (template.type === "Workflow Job Template" ? 'workflow ' : '') + 'job template below?
' + $filter('sanitize')(template.name) + '
', + body: '
Are you sure you want to delete the template below?
' + $filter('sanitize')(template.name) + '
', action: function() { function handleSuccessfulDelete() { From e2ea73aa35fe62b4629229921f6fb28dc1253274 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 12 Dec 2016 11:18:22 -0800 Subject: [PATCH 097/595] making labels collapsible on workflow-results to match the job results page --- .../workflow-results.controller.js | 12 ++++++++ .../workflow-results.partial.html | 28 +++++++++++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/workflow-results/workflow-results.controller.js b/awx/ui/client/src/workflow-results/workflow-results.controller.js index 3a375324a9..58dfc3a92f 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.controller.js +++ b/awx/ui/client/src/workflow-results/workflow-results.controller.js @@ -121,6 +121,18 @@ export default ['workflowData', $scope.showManualControls = !$scope.showManualControls; }; + $scope.lessLabels = false; + $scope.toggleLessLabels = function() { + if (!$scope.lessLabels) { + $('#workflow-results-labels').slideUp(200); + $scope.lessLabels = true; + } + else { + $('#workflow-results-labels').slideDown(200); + $scope.lessLabels = false; + } + }; + $scope.panChart = function(direction) { $scope.$broadcast('panWorkflowChart', { direction: direction diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index 5923bf8132..ac04b114e8 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -147,11 +147,27 @@
- - +
+ +
From e895c0989af0ba8946e278a881e3f36e0a45c6c3 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Mon, 12 Dec 2016 14:51:09 -0500 Subject: [PATCH 098/595] Fixes routing to scheduled jobs view (#4280) * Issue #4267 * Fixes routing issues to upcoming scheduled jobs view * Fixes edit schedule routing from scheduled jobs list view * re-enable jobDetails route, match jobDetais route only when url is /jobs/integer/ --- awx/ui/client/src/inventories/main.js | 2 +- awx/ui/client/src/lists/Schedules.js | 6 +- awx/ui/client/src/partials/jobs.html | 2 +- awx/ui/client/src/scheduler/main.js | 11 ++- .../src/scheduler/schedulerList.controller.js | 75 ++++++++++++++++++- 5 files changed, 84 insertions(+), 12 deletions(-) diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 191bfbb17c..aa2b450332 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -83,7 +83,7 @@ angular.module('inventory', [ mode: 'edit' }); html = generateList.wrapPanel(html); - return html; + return generateList.insertFormView() + html; }, controller: 'schedulerListController' } diff --git a/awx/ui/client/src/lists/Schedules.js b/awx/ui/client/src/lists/Schedules.js index 30d0ceb4e5..fbf03f5678 100644 --- a/awx/ui/client/src/lists/Schedules.js +++ b/awx/ui/client/src/lists/Schedules.js @@ -31,7 +31,7 @@ export default name: { key: true, label: 'Name', - ngClick: "editSchedule(schedule.id)", + ngClick: "editSchedule(schedule)", columnClass: "col-md-3 col-sm-3 col-xs-6" }, dtstart: { @@ -73,7 +73,7 @@ export default fieldActions: { edit: { label: 'Edit', - ngClick: "editSchedule(schedule.id)", + ngClick: "editSchedule(schedule)", icon: 'icon-edit', awToolTip: 'Edit schedule', dataPlacement: 'top', @@ -81,7 +81,7 @@ export default }, view: { label: 'View', - ngClick: "editSchedule(schedule.id)", + ngClick: "editSchedule(schedule)", awToolTip: 'View schedule', dataPlacement: 'top', ngShow: '!schedule.summary_fields.user_capabilities.edit' diff --git a/awx/ui/client/src/partials/jobs.html b/awx/ui/client/src/partials/jobs.html index 1477b87ab7..00de9af821 100644 --- a/awx/ui/client/src/partials/jobs.html +++ b/awx/ui/client/src/partials/jobs.html @@ -6,7 +6,7 @@ - diff --git a/awx/ui/client/src/scheduler/main.js b/awx/ui/client/src/scheduler/main.js index 3acd6a4d79..840197d944 100644 --- a/awx/ui/client/src/scheduler/main.js +++ b/awx/ui/client/src/scheduler/main.js @@ -238,13 +238,13 @@ export default // upcoming scheduled jobs $stateExtender.addState({ searchPrefix: 'schedule', - name: 'jobs.scheduled', - route: '/scheduled', + name: 'jobs.schedules', + route: '/schedules', params: { schedule_search: { value: { next_run__isnull: 'false', - order_by: 'next_run' + order_by: 'unified_job_template__polymorphic_ctype__model' } } }, @@ -258,13 +258,16 @@ export default label: 'SCHEDULED' }, resolve: { + SchedulesList: ['ScheduledJobsList', function(list){ + return list; + }], Dataset: ['SchedulesList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { let path = GetBasePath('schedules'); return qs.search(path, $stateParams[`${list.iterator}_search`]); } ], - ParentObject: [() =>{return null;}] + ParentObject: [() =>{return {endpoint:'/api/v1/schedules'}; }], }, views: { 'list@jobs': { diff --git a/awx/ui/client/src/scheduler/schedulerList.controller.js b/awx/ui/client/src/scheduler/schedulerList.controller.js index b532c6d6dd..e003ea1fd2 100644 --- a/awx/ui/client/src/scheduler/schedulerList.controller.js +++ b/awx/ui/client/src/scheduler/schedulerList.controller.js @@ -31,7 +31,7 @@ export default [ function init() { if (ParentObject){ $scope.parentObject = ParentObject; - scheduleEndpoint = ParentObject.related.schedules || `${ParentObject.related.inventory_source}schedules`; + scheduleEndpoint = ParentObject.endpoint|| ParentObject.related.schedules || `${ParentObject.related.inventory_source}schedules`; $scope.canAdd = false; rbacUiControlService.canAdd(scheduleEndpoint) .then(function(canAdd) { @@ -67,8 +67,77 @@ export default [ $state.go('.add'); }; - $scope.editSchedule = function(schedule_id) { - $state.go('.edit', { schedule_id: schedule_id }); + $scope.editSchedule = function(schedule) { + if ($state.is('jobs.schedules')){ + routeToScheduleForm(schedule, 'edit'); + } + else { + $state.go('.edit', { schedule_id: schedule.id }); + } + + function buildStateMap(schedule){ + + let deferred = $q.defer(); + + switch(schedule.summary_fields.unified_job_template.unified_job_type){ + case 'inventory_update': + Rest.setUrl(schedule.related.unified_job_template); + Rest.get().then( (res) => { + deferred.resolve({ + name: 'inventoryManage.editGroup.schedules.edit', + params: { + group_id: res.data.group, + inventory_id: res.data.inventory, + schedule_id: schedule.id, + } + }); + }); + break; + + case 'project_update': + deferred.resolve({ + name: 'projectSchedules.edit', + params: { + id: schedule.unified_job_template, + schedule_id: schedule.id + } + }); + break; + + case 'system_job': + deferred.resolve({ + name: 'managementJobSchedules.edit', + params: { + id: schedule.unified_job_template, + schedule_id: schedule.id + } + }); + break; + } + + return deferred.promise; + } + + function routeToScheduleForm(schedule){ + + buildStateMap(schedule).then( (state) =>{ + $state.go(state.name, state.params).catch((err) =>{ + // stateDefinition.lazyLoad'd state name matcher is not configured to match inventoryManage.* state names + // However, the stateDefinition.lazyLoad url matcher will load the correct tree. + // Expected error: + // Transition rejection error + // type: 4 // SUPERSEDED = 2, ABORTED = 3, INVALID = 4, IGNORED = 5, ERROR = 6 + // detail : "Could not resolve 'inventoryManage.editGroup.schedules.edit' from state 'jobs.schedules'" + // message: "This transition is invalid" + if (err.type === 4 && err.detail.includes('inventoryManage.editGroup.schedules.edit')){ + $location.path(`/inventories/${state.params.inventory_id}/manage/edit-group/${state.params.group_id}/schedules/${state.params.schedule_id}`); + } + else { + throw err; + } + }); + }); + } }; $scope.toggleSchedule = function(event, id) { From 08286d6d06d40b341b87df1035dba5b41cfbb13c Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 12 Dec 2016 14:56:49 -0500 Subject: [PATCH 099/595] Add valid 1px GIF as placeholder. --- awx/ui/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/conf.py b/awx/ui/conf.py index bac1709253..a8f8a42766 100644 --- a/awx/ui/conf.py +++ b/awx/ui/conf.py @@ -49,7 +49,7 @@ register( help_text=_('To set up a custom logo, provide a file that you create. For ' 'the custom logo to look its best, use a `.png` file with a ' 'transparent background. GIF, PNG and JPEG formats are supported.'), - placeholder='data:image/gif;base64,R0lGODlhAQABAAAAADs=', + placeholder='data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=', category=_('UI'), category_slug='ui', feature_required='rebranding', From 93ce2a5dfd1f9e030d6bfcc1edcec6f04d579213 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 12 Dec 2016 15:26:13 -0500 Subject: [PATCH 100/595] Set proot/bubblewrap enabled by default --- awx/settings/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 4a15abef7b..fbb9c8fb73 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -538,7 +538,7 @@ CAPTURE_JOB_EVENT_HOSTS = False # Enable bubblewrap support for running jobs (playbook runs only). # Note: This setting may be overridden by database settings. -AWX_PROOT_ENABLED = False +AWX_PROOT_ENABLED = True # Command/path to bubblewrap. AWX_PROOT_CMD = 'bwrap' From d6f1f2f22e6d65245ef2ed7ebe2c3a7fc676a33e Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Mon, 12 Dec 2016 15:50:40 -0500 Subject: [PATCH 101/595] Simplified and fixed save button permissions --- .../auth-form/configuration-auth.controller.js | 4 ++-- awx/ui/client/src/configuration/configuration.controller.js | 6 ------ .../jobs-form/configuration-jobs.controller.js | 4 ++-- .../system-form/configuration-system.controller.js | 4 ++-- .../configuration/ui-form/configuration-ui.controller.js | 4 ++-- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js index c38183599e..33d1a53c37 100644 --- a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js @@ -157,8 +157,8 @@ export default [ } addFieldInfo(form, key); }); - // Disable the save button for non-superusers - form.buttons.save.disabled = 'vm.updateProhibited'; + // Disable the save button for system auditors + form.buttons.save.disabled = $rootScope.user_is_system_auditor; }); function addFieldInfo(form, key) { diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js index 35d19a5611..e5d3d6049d 100644 --- a/awx/ui/client/src/configuration/configuration.controller.js +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -465,11 +465,6 @@ export default [ triggerModal(msg, title, buttons); }; - var updateProhibited = true; - if($rootScope.user_is_superuser) { - updateProhibited = false; - } - angular.extend(vm, { activeTab: activeTab, activeTabCheck: activeTabCheck, @@ -482,7 +477,6 @@ export default [ resetAllConfirm: resetAllConfirm, show_auditor_bar: show_auditor_bar, triggerModal: triggerModal, - updateProhibited: updateProhibited }); } ]; diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js index c242e97da1..0d7cd0962b 100644 --- a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js @@ -39,8 +39,8 @@ export default [ }); }); - // Disable the save button for non-superusers - form.buttons.save.disabled = 'vm.updateProhibited'; + // Disable the save button for system auditors + form.buttons.save.disabled = $rootScope.user_is_system_auditor; var keys = _.keys(form.fields); _.each(keys, function(key) { diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js index 3751e298a5..c22131c2bc 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js +++ b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js @@ -19,8 +19,8 @@ export default [ addFieldInfo(form, key); }); - // Disable the save button for non-superusers - form.buttons.save.disabled = 'vm.updateProhibited'; + // Disable the save button for system auditors + form.buttons.save.disabled = $rootScope.user_is_system_auditor; function addFieldInfo(form, key) { _.extend(form.fields[key], { diff --git a/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js b/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js index aff68d72dc..bfa19b165f 100644 --- a/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js +++ b/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js @@ -47,8 +47,8 @@ addFieldInfo(form, key); }); - // Disable the save button for non-superusers - form.buttons.save.disabled = 'vm.updateProhibited'; + // Disable the save button for system auditors + form.buttons.save.disabled = $rootScope.user_is_system_auditor; function addFieldInfo(form, key) { _.extend(form.fields[key], { From 3597c34593d2b1ffacc125dbd2ec502e4fdc5bc0 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 12 Dec 2016 15:54:44 -0500 Subject: [PATCH 102/595] Rename .te filename to reflect selinux policy name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://patchwork.kernel.org/patch/8784071/ While renaming the policy would have been a “cleaner” solution, this would create a scenario where our setup playbooks would need to check for the policy by it’s old name before installing the new one. Updating the filename to reflect the policy name is the least invasive and risky change. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 27aa0419f2..b32cf7b35c 100644 --- a/Makefile +++ b/Makefile @@ -689,7 +689,7 @@ rpm-build: rpm-build/$(SDIST_TAR_FILE): rpm-build dist/$(SDIST_TAR_FILE) cp packaging/rpm/$(NAME).spec rpm-build/ - cp packaging/rpm/$(NAME).te rpm-build/ + cp packaging/rpm/tower.te rpm-build/ cp packaging/rpm/$(NAME).sysconfig rpm-build/ cp packaging/remove_tower_source.py rpm-build/ cp packaging/bytecompile.sh rpm-build/ From e1942083e31602528874f11448f0dc9d25e209b5 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 12 Dec 2016 16:13:56 -0500 Subject: [PATCH 103/595] Remove sneeringer's old unused decorator code --- awx/api/utils/__init__.py | 0 awx/api/utils/decorators.py | 82 ------------------ awx/api/views.py | 14 ++-- .../tests/unit/api/decorator_paginated.py | 83 ------------------- 4 files changed, 8 insertions(+), 171 deletions(-) delete mode 100644 awx/api/utils/__init__.py delete mode 100644 awx/api/utils/decorators.py delete mode 100644 awx/main/tests/unit/api/decorator_paginated.py diff --git a/awx/api/utils/__init__.py b/awx/api/utils/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/awx/api/utils/decorators.py b/awx/api/utils/decorators.py deleted file mode 100644 index 8a80b1457e..0000000000 --- a/awx/api/utils/decorators.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -from collections import OrderedDict -import copy -import functools - -from rest_framework.response import Response -from rest_framework.settings import api_settings -from rest_framework import status - - -def paginated(method): - """Given an method with a Django REST Framework API method signature - (e.g. `def get(self, request, ...):`), abstract out boilerplate pagination - duties. - - This causes the method to receive two additional keyword arguments: - `limit`, and `offset`. The method expects a two-tuple to be - returned, with a result list as the first item, and the total number - of results (across all pages) as the second item. - """ - @functools.wraps(method) - def func(self, request, *args, **kwargs): - # Manually spin up pagination. - # How many results do we show? - paginator_class = api_settings.DEFAULT_PAGINATION_CLASS - limit = paginator_class.page_size - if request.query_params.get(paginator_class.page_size_query_param, False): - limit = request.query_params[paginator_class.page_size_query_param] - if paginator_class.max_page_size: - limit = min(paginator_class.max_page_size, limit) - limit = int(limit) - - # Get the order parameter if it's given - if request.query_params.get("ordering", False): - ordering = request.query_params["ordering"] - else: - ordering = None - - # What page are we on? - page = int(request.query_params.get('page', 1)) - offset = (page - 1) * limit - - # Add the limit, offset, page, and order variables to the keyword arguments - # being sent to the underlying method. - kwargs['limit'] = limit - kwargs['offset'] = offset - kwargs['ordering'] = ordering - - # Okay, call the underlying method. - results, count, stat = method(self, request, *args, **kwargs) - if stat is None: - stat = status.HTTP_200_OK - - if stat == status.HTTP_200_OK: - # Determine the next and previous pages, if any. - prev, next_ = None, None - if page > 1: - get_copy = copy.copy(request.GET) - get_copy['page'] = page - 1 - prev = '%s?%s' % (request.path, get_copy.urlencode()) - if count > offset + limit: - get_copy = copy.copy(request.GET) - get_copy['page'] = page + 1 - next_ = '%s?%s' % (request.path, get_copy.urlencode()) - - # Compile the results into a dictionary with pagination - # information. - answer = OrderedDict(( - ('count', count), - ('next', next_), - ('previous', prev), - ('results', results), - )) - else: - answer = results - - # Okay, we're done; return response data. - return Response(answer, status=stat) - return func - diff --git a/awx/api/views.py b/awx/api/views.py index eda37db70f..1ae1d03ea7 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -60,7 +60,6 @@ from awx.main.tasks import send_notifications from awx.main.access import get_user_queryset from awx.main.ha import is_ha_environment from awx.api.authentication import TaskAuthentication, TokenGetAuthentication -from awx.api.utils.decorators import paginated from awx.api.generics import get_view_name from awx.api.generics import * # noqa from awx.conf.license import get_license, feature_enabled, feature_exists, LicenseForbids @@ -3435,8 +3434,10 @@ class JobJobPlaysList(BaseJobEventsList): view_name = _('Job Plays List') new_in_200 = True - @paginated - def get(self, request, limit, offset, ordering, *args, **kwargs): + def get(self, request, *args, **kwargs): + limit = kwargs.get('limit', 20) + ordering = kwargs.get('ordering', None) + offset = kwargs.get('offset', 0) all_plays = [] job = Job.objects.filter(pk=self.kwargs['pk']) if not job.exists(): @@ -3510,14 +3511,15 @@ class JobJobTasksList(BaseJobEventsList): view_name = _('Job Play Tasks List') new_in_200 = True - @paginated - def get(self, request, limit, offset, ordering, *args, **kwargs): + def get(self, request, *args, **kwargs): """Return aggregate data about each of the job tasks that is: - an immediate child of the job event - corresponding to the spinning up of a new task or playbook """ results = [] - + limit = kwargs.get('limit', 20) + ordering = kwargs.get('ordering', None) + offset = kwargs.get('offset', 0) # Get the job and the parent task. # If there's no event ID specified, this will return a 404. job = Job.objects.filter(pk=self.kwargs['pk']) diff --git a/awx/main/tests/unit/api/decorator_paginated.py b/awx/main/tests/unit/api/decorator_paginated.py deleted file mode 100644 index 71344e92ba..0000000000 --- a/awx/main/tests/unit/api/decorator_paginated.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -import json - -from django.test import TestCase - -from rest_framework.permissions import AllowAny -from rest_framework.test import APIRequestFactory -from rest_framework.views import APIView - -from awx.api.utils.decorators import paginated - - -class PaginatedDecoratorTests(TestCase): - """A set of tests for ensuring that the "paginated" decorator works - in the way we expect. - """ - def setUp(self): - self.rf = APIRequestFactory() - - # Define an uninteresting view that we can use to test - # that the paginator wraps in the way we expect. - class View(APIView): - permission_classes = (AllowAny,) - - @paginated - def get(self, request, limit, ordering, offset): - return ['a', 'b', 'c', 'd', 'e'], 26, None - self.view = View.as_view() - - def test_implicit_first_page(self): - """Establish that if we get an implicit request for the first page - (e.g. no page provided), that it is returned appropriately. - """ - # Create a request, and run the paginated function. - request = self.rf.get('/dummy/', {'page_size': 5}) - response = self.view(request) - - # Ensure the response looks like what it should. - r = json.loads(response.rendered_content) - self.assertEqual(r['count'], 26) - self.assertIn(r['next'], - (u'/dummy/?page=2&page_size=5', - u'/dummy/?page_size=5&page=2')) - self.assertEqual(r['previous'], None) - self.assertEqual(r['results'], ['a', 'b', 'c', 'd', 'e']) - - def test_mid_page(self): - """Establish that if we get a request for a page in the middle, that - the paginator causes next and prev to be set appropriately. - """ - # Create a request, and run the paginated function. - request = self.rf.get('/dummy/', {'page': 3, 'page_size': 5}) - response = self.view(request) - - # Ensure the response looks like what it should. - r = json.loads(response.rendered_content) - self.assertEqual(r['count'], 26) - self.assertIn(r['next'], - (u'/dummy/?page=4&page_size=5', - u'/dummy/?page_size=5&page=4')) - self.assertIn(r['previous'], - (u'/dummy/?page=2&page_size=5', - u'/dummy/?page_size=5&page=2')) - self.assertEqual(r['results'], ['a', 'b', 'c', 'd', 'e']) - - def test_last_page(self): - """Establish that if we get a request for the last page, that the - paginator picks up on it and sets `next` to None. - """ - # Create a request, and run the paginated function. - request = self.rf.get('/dummy/', {'page': 6, 'page_size': 5}) - response = self.view(request) - - # Ensure the response looks like what it should. - r = json.loads(response.rendered_content) - self.assertEqual(r['count'], 26) - self.assertEqual(r['next'], None) - self.assertIn(r['previous'], - (u'/dummy/?page=5&page_size=5', - u'/dummy/?page_size=5&page=5')) - self.assertEqual(r['results'], ['a', 'b', 'c', 'd', 'e']) From b85c98afd2927c7d67f1d105ebd86fb27c977ed1 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 12 Dec 2016 16:34:36 -0500 Subject: [PATCH 104/595] Split job event data between callback queue and stdout. Send most of event data directly over queue and capture only stdout/counter/start_line/end_line in celery task; recombine into single event in callback receiver. --- awx/lib/tower_display_callback/display.py | 2 +- awx/lib/tower_display_callback/events.py | 66 ++++++++++++++++++- awx/lib/tower_display_callback/module.py | 4 +- .../commands/run_callback_receiver.py | 23 +++++-- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/awx/lib/tower_display_callback/display.py b/awx/lib/tower_display_callback/display.py index 128c9349c7..ad5e8ba37a 100644 --- a/awx/lib/tower_display_callback/display.py +++ b/awx/lib/tower_display_callback/display.py @@ -26,7 +26,7 @@ import uuid from ansible.utils.display import Display # Tower Display Callback -from tower_display_callback.events import event_context +from .events import event_context __all__ = [] diff --git a/awx/lib/tower_display_callback/events.py b/awx/lib/tower_display_callback/events.py index 86fab2895b..0909ed460d 100644 --- a/awx/lib/tower_display_callback/events.py +++ b/awx/lib/tower_display_callback/events.py @@ -22,14 +22,75 @@ import base64 import contextlib import datetime import json +import logging import multiprocessing import os import threading import uuid +# Kombu +from kombu import Connection, Exchange, Producer + __all__ = ['event_context'] +class CallbackQueueEventDispatcher(object): + + def __init__(self): + self.callback_connection = os.getenv('CALLBACK_CONNECTION', None) + self.connection_queue = os.getenv('CALLBACK_QUEUE', '') + self.connection = None + self.exchange = None + self._init_logging() + + def _init_logging(self): + try: + self.job_callback_debug = int(os.getenv('JOB_CALLBACK_DEBUG', '0')) + except ValueError: + self.job_callback_debug = 0 + self.logger = logging.getLogger('awx.plugins.callback.job_event_callback') + if self.job_callback_debug >= 2: + self.logger.setLevel(logging.DEBUG) + elif self.job_callback_debug >= 1: + self.logger.setLevel(logging.INFO) + else: + self.logger.setLevel(logging.WARNING) + handler = logging.StreamHandler() + formatter = logging.Formatter('%(levelname)-8s %(process)-8d %(message)s') + handler.setFormatter(formatter) + self.logger.addHandler(handler) + self.logger.propagate = False + + def dispatch(self, obj): + if not self.callback_connection or not self.connection_queue: + return + active_pid = os.getpid() + for retry_count in xrange(4): + try: + if not hasattr(self, 'connection_pid'): + self.connection_pid = active_pid + if self.connection_pid != active_pid: + self.connection = None + if self.connection is None: + self.connection = Connection(self.callback_connection) + self.exchange = Exchange(self.connection_queue, type='direct') + + producer = Producer(self.connection) + producer.publish(obj, + serializer='json', + compression='bzip2', + exchange=self.exchange, + declare=[self.exchange], + routing_key=self.connection_queue) + return + except Exception, e: + self.logger.info('Publish Job Event Exception: %r, retry=%d', e, + retry_count, exc_info=True) + retry_count += 1 + if retry_count >= 3: + break + + class EventContext(object): ''' Store global and local (per thread/process) data associated with callback @@ -38,6 +99,7 @@ class EventContext(object): def __init__(self): self.display_lock = multiprocessing.RLock() + self.dispatcher = CallbackQueueEventDispatcher() def add_local(self, **kwargs): if not hasattr(self, '_local'): @@ -136,7 +198,9 @@ class EventContext(object): fileobj.flush() def dump_begin(self, fileobj): - self.dump(fileobj, self.get_begin_dict()) + begin_dict = self.get_begin_dict() + self.dispatcher.dispatch(begin_dict) + self.dump(fileobj, {'uuid': begin_dict['uuid']}) def dump_end(self, fileobj): self.dump(fileobj, self.get_end_dict(), flush=True) diff --git a/awx/lib/tower_display_callback/module.py b/awx/lib/tower_display_callback/module.py index e61ef17624..59faa7ac79 100644 --- a/awx/lib/tower_display_callback/module.py +++ b/awx/lib/tower_display_callback/module.py @@ -29,8 +29,8 @@ from ansible.plugins.callback import CallbackBase from ansible.plugins.callback.default import CallbackModule as DefaultCallbackModule # Tower Display Callback -from tower_display_callback.events import event_context -from tower_display_callback.minimal import CallbackModule as MinimalCallbackModule +from .events import event_context +from .minimal import CallbackModule as MinimalCallbackModule class BaseCallbackModule(CallbackBase): diff --git a/awx/main/management/commands/run_callback_receiver.py b/awx/main/management/commands/run_callback_receiver.py index c0105b2587..7b959f6781 100644 --- a/awx/main/management/commands/run_callback_receiver.py +++ b/awx/main/management/commands/run_callback_receiver.py @@ -21,6 +21,7 @@ logger = logging.getLogger('awx.main.commands.run_callback_receiver') class CallbackBrokerWorker(ConsumerMixin): def __init__(self, connection): self.connection = connection + self.partial_events = {} def get_consumers(self, Consumer, channel): return [Consumer(queues=[Queue(settings.CALLBACK_QUEUE, @@ -31,18 +32,28 @@ class CallbackBrokerWorker(ConsumerMixin): def process_task(self, body, message): try: - if 'event' not in body: - raise Exception('Payload does not have an event') if 'job_id' not in body and 'ad_hoc_command_id' not in body: raise Exception('Payload does not have a job_id or ad_hoc_command_id') if settings.DEBUG: logger.info('Body: {}'.format(body)) logger.info('Message: {}'.format(message)) try: - if 'job_id' in body: - JobEvent.create_from_data(**body) - elif 'ad_hoc_command_id' in body: - AdHocCommandEvent.create_from_data(**body) + # If event came directly from callback without counter/stdout, + # save it until the rest of the event arrives. + if 'counter' not in body: + if 'uuid' in body: + self.partial_events[body['uuid']] = body + # If event has counter, try to combine it with any event data + # already received for the same uuid, then create the actual + # job event record. + else: + if 'uuid' in body: + partial_event = self.partial_events.pop(body['uuid'], {}) + body.update(partial_event) + if 'job_id' in body: + JobEvent.create_from_data(**body) + elif 'ad_hoc_command_id' in body: + AdHocCommandEvent.create_from_data(**body) except DatabaseError as e: logger.error('Database Error Saving Job Event: {}'.format(e)) except Exception as exc: From 783e86bd41e912ab43b3bf3e2170d17a6670a53f Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 12 Dec 2016 14:44:22 -0500 Subject: [PATCH 105/595] Consolidate 3.1.0 migrations --- .../migrations/0034_v310_add_workflows.py | 109 ---- awx/main/migrations/0034_v310_release.py | 611 ++++++++++++++++++ .../0035_v310_modify_ha_instance.py | 23 - .../migrations/0036_v310_jobevent_uuid.py | 19 - .../0037_v310_remove_tower_settings.py | 22 - .../0038_v310_job_allow_simultaneous.py | 19 - .../0039_v310_workflow_rbac_prompts.py | 82 --- awx/main/migrations/0040_v310_channelgroup.py | 22 - awx/main/migrations/0041_v310_artifacts.py | 25 - awx/main/migrations/0042_v310_job_timeout.py | 44 -- .../migrations/0043_v310_executionnode.py | 19 - awx/main/migrations/0044_v310_scm_revision.py | 30 - .../0045_v310_project_playbook_files.py | 20 - .../migrations/0046_v310_job_event_stdout.py | 96 --- awx/main/migrations/0047_v310_tower_state.py | 24 - .../migrations/0048_v310_instance_capacity.py | 19 - .../migrations/0049_v310_workflow_surveys.py | 30 - .../migrations/0050_v310_JSONField_changes.py | 90 --- .../0051_v310_job_project_update.py | 20 - .../0052_v310_inventory_name_non_unique.py | 19 - .../0053_v310_update_timeout_field_type.py | 44 -- .../migrations/0054_text_and_has_schedules.py | 133 ---- 22 files changed, 611 insertions(+), 909 deletions(-) delete mode 100644 awx/main/migrations/0034_v310_add_workflows.py create mode 100644 awx/main/migrations/0034_v310_release.py delete mode 100644 awx/main/migrations/0035_v310_modify_ha_instance.py delete mode 100644 awx/main/migrations/0036_v310_jobevent_uuid.py delete mode 100644 awx/main/migrations/0037_v310_remove_tower_settings.py delete mode 100644 awx/main/migrations/0038_v310_job_allow_simultaneous.py delete mode 100644 awx/main/migrations/0039_v310_workflow_rbac_prompts.py delete mode 100644 awx/main/migrations/0040_v310_channelgroup.py delete mode 100644 awx/main/migrations/0041_v310_artifacts.py delete mode 100644 awx/main/migrations/0042_v310_job_timeout.py delete mode 100644 awx/main/migrations/0043_v310_executionnode.py delete mode 100644 awx/main/migrations/0044_v310_scm_revision.py delete mode 100644 awx/main/migrations/0045_v310_project_playbook_files.py delete mode 100644 awx/main/migrations/0046_v310_job_event_stdout.py delete mode 100644 awx/main/migrations/0047_v310_tower_state.py delete mode 100644 awx/main/migrations/0048_v310_instance_capacity.py delete mode 100644 awx/main/migrations/0049_v310_workflow_surveys.py delete mode 100644 awx/main/migrations/0050_v310_JSONField_changes.py delete mode 100644 awx/main/migrations/0051_v310_job_project_update.py delete mode 100644 awx/main/migrations/0052_v310_inventory_name_non_unique.py delete mode 100644 awx/main/migrations/0053_v310_update_timeout_field_type.py delete mode 100644 awx/main/migrations/0054_text_and_has_schedules.py diff --git a/awx/main/migrations/0034_v310_add_workflows.py b/awx/main/migrations/0034_v310_add_workflows.py deleted file mode 100644 index 4dfb84177a..0000000000 --- a/awx/main/migrations/0034_v310_add_workflows.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import awx.main.models.notifications -import django.db.models.deletion -import awx.main.models.workflow -import awx.main.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0033_v303_v245_host_variable_fix'), - ] - - operations = [ - migrations.AlterField( - model_name='unifiedjob', - name='launch_type', - field=models.CharField(default=b'manual', max_length=20, editable=False, choices=[(b'manual', 'Manual'), (b'relaunch', 'Relaunch'), (b'callback', 'Callback'), (b'scheduled', 'Scheduled'), (b'dependency', 'Dependency'), (b'workflow', 'Workflow')]), - ), - migrations.CreateModel( - name='WorkflowJob', - fields=[ - ('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')), - ('extra_vars', models.TextField(default=b'', blank=True)), - ], - options={ - 'ordering': ('id',), - }, - bases=('main.unifiedjob', models.Model, awx.main.models.notifications.JobNotificationMixin, awx.main.models.workflow.WorkflowJobInheritNodesMixin), - ), - migrations.CreateModel( - name='WorkflowJobNode', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', models.DateTimeField(default=None, editable=False)), - ('modified', models.DateTimeField(default=None, editable=False)), - ('always_nodes', models.ManyToManyField(related_name='workflowjobnodes_always', to='main.WorkflowJobNode', blank=True)), - ('failure_nodes', models.ManyToManyField(related_name='workflowjobnodes_failure', to='main.WorkflowJobNode', blank=True)), - ('job', models.OneToOneField(related_name='unified_job_node', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.UnifiedJob', null=True)), - ('success_nodes', models.ManyToManyField(related_name='workflowjobnodes_success', to='main.WorkflowJobNode', blank=True)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='WorkflowJobTemplate', - fields=[ - ('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')), - ('extra_vars', models.TextField(default=b'', blank=True)), - ('admin_role', awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'singleton:system_administrator', to='main.Role', null=b'True')), - ], - bases=('main.unifiedjobtemplate', models.Model), - ), - migrations.CreateModel( - name='WorkflowJobTemplateNode', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', models.DateTimeField(default=None, editable=False)), - ('modified', models.DateTimeField(default=None, editable=False)), - ('always_nodes', models.ManyToManyField(related_name='workflowjobtemplatenodes_always', to='main.WorkflowJobTemplateNode', blank=True)), - ('failure_nodes', models.ManyToManyField(related_name='workflowjobtemplatenodes_failure', to='main.WorkflowJobTemplateNode', blank=True)), - ('success_nodes', models.ManyToManyField(related_name='workflowjobtemplatenodes_success', to='main.WorkflowJobTemplateNode', blank=True)), - ('unified_job_template', models.ForeignKey(related_name='workflowjobtemplatenodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.UnifiedJobTemplate', null=True)), - ('workflow_job_template', models.ForeignKey(related_name='workflow_job_template_nodes', default=None, blank=True, to='main.WorkflowJobTemplate', null=True)), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='workflowjobnode', - name='unified_job_template', - field=models.ForeignKey(related_name='workflowjobnodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.UnifiedJobTemplate', null=True), - ), - migrations.AddField( - model_name='workflowjobnode', - name='workflow_job', - field=models.ForeignKey(related_name='workflow_job_nodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.WorkflowJob', null=True), - ), - migrations.AddField( - model_name='workflowjob', - name='workflow_job_template', - field=models.ForeignKey(related_name='workflow_jobs', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.WorkflowJobTemplate', null=True), - ), - migrations.AddField( - model_name='activitystream', - name='workflow_job', - field=models.ManyToManyField(to='main.WorkflowJob', blank=True), - ), - migrations.AddField( - model_name='activitystream', - name='workflow_job_node', - field=models.ManyToManyField(to='main.WorkflowJobNode', blank=True), - ), - migrations.AddField( - model_name='activitystream', - name='workflow_job_template', - field=models.ManyToManyField(to='main.WorkflowJobTemplate', blank=True), - ), - migrations.AddField( - model_name='activitystream', - name='workflow_job_template_node', - field=models.ManyToManyField(to='main.WorkflowJobTemplateNode', blank=True), - ), - ] diff --git a/awx/main/migrations/0034_v310_release.py b/awx/main/migrations/0034_v310_release.py new file mode 100644 index 0000000000..e86691d2e1 --- /dev/null +++ b/awx/main/migrations/0034_v310_release.py @@ -0,0 +1,611 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import awx.main.models.notifications +import jsonfield.fields +import django.db.models.deletion +import awx.main.models.workflow +import awx.main.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0033_v303_v245_host_variable_fix'), + ] + + operations = [ + # Create ChannelGroup table + migrations.CreateModel( + name='ChannelGroup', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('group', models.CharField(unique=True, max_length=200)), + ('channels', models.TextField()), + ], + ), + # Allow simultaneous Job + migrations.AddField( + model_name='job', + name='allow_simultaneous', + field=models.BooleanField(default=False), + ), + # Remove Tower settings, these settings are now in separate awx.conf app. + migrations.RemoveField( + model_name='towersettings', + name='user', + ), + migrations.DeleteModel( + name='TowerSettings', + ), + # Job Event UUID + migrations.AddField( + model_name='jobevent', + name='uuid', + field=models.CharField(default=b'', max_length=1024, editable=False), + ), + # Modify the HA Instance + migrations.RemoveField( + model_name='instance', + name='primary', + ), + migrations.AlterField( + model_name='instance', + name='uuid', + field=models.CharField(max_length=40), + ), + # Add Workflows + migrations.AlterField( + model_name='unifiedjob', + name='launch_type', + field=models.CharField(default=b'manual', max_length=20, editable=False, choices=[(b'manual', 'Manual'), (b'relaunch', 'Relaunch'), (b'callback', 'Callback'), (b'scheduled', 'Scheduled'), (b'dependency', 'Dependency'), (b'workflow', 'Workflow')]), + ), + migrations.CreateModel( + name='WorkflowJob', + fields=[ + ('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')), + ('extra_vars', models.TextField(default=b'', blank=True)), + ], + options={ + 'ordering': ('id',), + }, + bases=('main.unifiedjob', models.Model, awx.main.models.notifications.JobNotificationMixin, awx.main.models.workflow.WorkflowJobInheritNodesMixin), + ), + migrations.CreateModel( + name='WorkflowJobNode', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', models.DateTimeField(default=None, editable=False)), + ('modified', models.DateTimeField(default=None, editable=False)), + ('always_nodes', models.ManyToManyField(related_name='workflowjobnodes_always', to='main.WorkflowJobNode', blank=True)), + ('failure_nodes', models.ManyToManyField(related_name='workflowjobnodes_failure', to='main.WorkflowJobNode', blank=True)), + ('job', models.OneToOneField(related_name='unified_job_node', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.UnifiedJob', null=True)), + ('success_nodes', models.ManyToManyField(related_name='workflowjobnodes_success', to='main.WorkflowJobNode', blank=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='WorkflowJobTemplate', + fields=[ + ('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')), + ('extra_vars', models.TextField(default=b'', blank=True)), + ('admin_role', awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'singleton:system_administrator', to='main.Role', null=b'True')), + ], + bases=('main.unifiedjobtemplate', models.Model), + ), + migrations.CreateModel( + name='WorkflowJobTemplateNode', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', models.DateTimeField(default=None, editable=False)), + ('modified', models.DateTimeField(default=None, editable=False)), + ('always_nodes', models.ManyToManyField(related_name='workflowjobtemplatenodes_always', to='main.WorkflowJobTemplateNode', blank=True)), + ('failure_nodes', models.ManyToManyField(related_name='workflowjobtemplatenodes_failure', to='main.WorkflowJobTemplateNode', blank=True)), + ('success_nodes', models.ManyToManyField(related_name='workflowjobtemplatenodes_success', to='main.WorkflowJobTemplateNode', blank=True)), + ('unified_job_template', models.ForeignKey(related_name='workflowjobtemplatenodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.UnifiedJobTemplate', null=True)), + ('workflow_job_template', models.ForeignKey(related_name='workflow_job_template_nodes', default=None, blank=True, to='main.WorkflowJobTemplate', null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='workflowjobnode', + name='unified_job_template', + field=models.ForeignKey(related_name='workflowjobnodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.UnifiedJobTemplate', null=True), + ), + migrations.AddField( + model_name='workflowjobnode', + name='workflow_job', + field=models.ForeignKey(related_name='workflow_job_nodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.WorkflowJob', null=True), + ), + migrations.AddField( + model_name='workflowjob', + name='workflow_job_template', + field=models.ForeignKey(related_name='workflow_jobs', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.WorkflowJobTemplate', null=True), + ), + migrations.AddField( + model_name='activitystream', + name='workflow_job', + field=models.ManyToManyField(to='main.WorkflowJob', blank=True), + ), + migrations.AddField( + model_name='activitystream', + name='workflow_job_node', + field=models.ManyToManyField(to='main.WorkflowJobNode', blank=True), + ), + migrations.AddField( + model_name='activitystream', + name='workflow_job_template', + field=models.ManyToManyField(to='main.WorkflowJobTemplate', blank=True), + ), + migrations.AddField( + model_name='activitystream', + name='workflow_job_template_node', + field=models.ManyToManyField(to='main.WorkflowJobTemplateNode', blank=True), + ), + # Workflow RBAC prompts + migrations.AddField( + model_name='workflowjobnode', + name='char_prompts', + field=jsonfield.fields.JSONField(default={}, blank=True), + ), + migrations.AddField( + model_name='workflowjobnode', + name='credential', + field=models.ForeignKey(related_name='workflowjobnodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True), + ), + migrations.AddField( + model_name='workflowjobnode', + name='inventory', + field=models.ForeignKey(related_name='workflowjobnodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Inventory', null=True), + ), + migrations.AddField( + model_name='workflowjobtemplate', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='workflowjobtemplate', + name='organization', + field=models.ForeignKey(related_name='workflows', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='main.Organization', null=True), + ), + migrations.AddField( + model_name='workflowjobtemplate', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'singleton:system_auditor', b'organization.auditor_role', b'execute_role', b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='workflowjobtemplatenode', + name='char_prompts', + field=jsonfield.fields.JSONField(default={}, blank=True), + ), + migrations.AddField( + model_name='workflowjobtemplatenode', + name='credential', + field=models.ForeignKey(related_name='workflowjobtemplatenodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True), + ), + migrations.AddField( + model_name='workflowjobtemplatenode', + name='inventory', + field=models.ForeignKey(related_name='workflowjobtemplatenodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Inventory', null=True), + ), + migrations.AlterField( + model_name='workflowjobnode', + name='unified_job_template', + field=models.ForeignKey(related_name='workflowjobnodes', on_delete=django.db.models.deletion.SET_NULL, default=None, to='main.UnifiedJobTemplate', null=True), + ), + migrations.AlterField( + model_name='workflowjobnode', + name='workflow_job', + field=models.ForeignKey(related_name='workflow_job_nodes', default=None, blank=True, to='main.WorkflowJob', null=True), + ), + migrations.AlterField( + model_name='workflowjobtemplate', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'singleton:system_administrator', b'organization.admin_role'], to='main.Role', null=b'True'), + ), + migrations.AlterField( + model_name='workflowjobtemplatenode', + name='unified_job_template', + field=models.ForeignKey(related_name='workflowjobtemplatenodes', on_delete=django.db.models.deletion.SET_NULL, default=None, to='main.UnifiedJobTemplate', null=True), + ), + # Job artifacts + migrations.AddField( + model_name='job', + name='artifacts', + field=jsonfield.fields.JSONField(default={}, editable=False, blank=True), + ), + migrations.AddField( + model_name='workflowjobnode', + name='ancestor_artifacts', + field=jsonfield.fields.JSONField(default={}, editable=False, blank=True), + ), + # Job timeout settings + migrations.AddField( + model_name='inventorysource', + name='timeout', + field=models.IntegerField(default=0, blank=True), + ), + migrations.AddField( + model_name='inventoryupdate', + name='timeout', + field=models.IntegerField(default=0, blank=True), + ), + migrations.AddField( + model_name='job', + name='timeout', + field=models.IntegerField(default=0, blank=True), + ), + migrations.AddField( + model_name='jobtemplate', + name='timeout', + field=models.IntegerField(default=0, blank=True), + ), + migrations.AddField( + model_name='project', + name='timeout', + field=models.IntegerField(default=0, blank=True), + ), + migrations.AddField( + model_name='projectupdate', + name='timeout', + field=models.IntegerField(default=0, blank=True), + ), + # Execution Node + migrations.AddField( + model_name='unifiedjob', + name='execution_node', + field=models.TextField(default=b'', editable=False, blank=True), + ), + # SCM Revision + migrations.AddField( + model_name='project', + name='scm_revision', + field=models.CharField(default=b'', editable=False, max_length=1024, blank=True, help_text='The last revision fetched by a project update', verbose_name='SCM Revision'), + ), + migrations.AddField( + model_name='projectupdate', + name='job_type', + field=models.CharField(default=b'check', max_length=64, choices=[(b'run', 'Run'), (b'check', 'Check')]), + ), + migrations.AddField( + model_name='job', + name='scm_revision', + field=models.CharField(default=b'', editable=False, max_length=1024, blank=True, help_text='The SCM Revision from the Project used for this job, if available', verbose_name='SCM Revision'), + ), + # Project Playbook Files + migrations.AddField( + model_name='project', + name='playbook_files', + field=jsonfield.fields.JSONField(default=[], help_text='List of playbooks found in the project', verbose_name='Playbook Files', editable=False, blank=True), + ), + # Job events to stdout + migrations.AddField( + model_name='adhoccommandevent', + name='end_line', + field=models.PositiveIntegerField(default=0, editable=False), + ), + migrations.AddField( + model_name='adhoccommandevent', + name='start_line', + field=models.PositiveIntegerField(default=0, editable=False), + ), + migrations.AddField( + model_name='adhoccommandevent', + name='stdout', + field=models.TextField(default=b'', editable=False), + ), + migrations.AddField( + model_name='adhoccommandevent', + name='uuid', + field=models.CharField(default=b'', max_length=1024, editable=False), + ), + migrations.AddField( + model_name='adhoccommandevent', + name='verbosity', + field=models.PositiveIntegerField(default=0, editable=False), + ), + migrations.AddField( + model_name='jobevent', + name='end_line', + field=models.PositiveIntegerField(default=0, editable=False), + ), + migrations.AddField( + model_name='jobevent', + name='playbook', + field=models.CharField(default=b'', max_length=1024, editable=False), + ), + migrations.AddField( + model_name='jobevent', + name='start_line', + field=models.PositiveIntegerField(default=0, editable=False), + ), + migrations.AddField( + model_name='jobevent', + name='stdout', + field=models.TextField(default=b'', editable=False), + ), + migrations.AddField( + model_name='jobevent', + name='verbosity', + field=models.PositiveIntegerField(default=0, editable=False), + ), + migrations.AlterField( + model_name='adhoccommandevent', + name='counter', + field=models.PositiveIntegerField(default=0, editable=False), + ), + migrations.AlterField( + model_name='adhoccommandevent', + name='event', + field=models.CharField(max_length=100, choices=[(b'runner_on_failed', 'Host Failed'), (b'runner_on_ok', 'Host OK'), (b'runner_on_unreachable', 'Host Unreachable'), (b'runner_on_skipped', 'Host Skipped'), (b'debug', 'Debug'), (b'verbose', 'Verbose'), (b'deprecated', 'Deprecated'), (b'warning', 'Warning'), (b'system_warning', 'System Warning'), (b'error', 'Error')]), + ), + migrations.AlterField( + model_name='jobevent', + name='counter', + field=models.PositiveIntegerField(default=0, editable=False), + ), + migrations.AlterField( + model_name='jobevent', + name='event', + field=models.CharField(max_length=100, choices=[(b'runner_on_failed', 'Host Failed'), (b'runner_on_ok', 'Host OK'), (b'runner_on_error', 'Host Failure'), (b'runner_on_skipped', 'Host Skipped'), (b'runner_on_unreachable', 'Host Unreachable'), (b'runner_on_no_hosts', 'No Hosts Remaining'), (b'runner_on_async_poll', 'Host Polling'), (b'runner_on_async_ok', 'Host Async OK'), (b'runner_on_async_failed', 'Host Async Failure'), (b'runner_item_on_ok', 'Item OK'), (b'runner_item_on_failed', 'Item Failed'), (b'runner_item_on_skipped', 'Item Skipped'), (b'runner_retry', 'Host Retry'), (b'runner_on_file_diff', 'File Difference'), (b'playbook_on_start', 'Playbook Started'), (b'playbook_on_notify', 'Running Handlers'), (b'playbook_on_include', 'Including File'), (b'playbook_on_no_hosts_matched', 'No Hosts Matched'), (b'playbook_on_no_hosts_remaining', 'No Hosts Remaining'), (b'playbook_on_task_start', 'Task Started'), (b'playbook_on_vars_prompt', 'Variables Prompted'), (b'playbook_on_setup', 'Gathering Facts'), (b'playbook_on_import_for_host', 'internal: on Import for Host'), (b'playbook_on_not_import_for_host', 'internal: on Not Import for Host'), (b'playbook_on_play_start', 'Play Started'), (b'playbook_on_stats', 'Playbook Complete'), (b'debug', 'Debug'), (b'verbose', 'Verbose'), (b'deprecated', 'Deprecated'), (b'warning', 'Warning'), (b'system_warning', 'System Warning'), (b'error', 'Error')]), + ), + migrations.AlterUniqueTogether( + name='adhoccommandevent', + unique_together=set([]), + ), + migrations.AlterIndexTogether( + name='adhoccommandevent', + index_together=set([('ad_hoc_command', 'event'), ('ad_hoc_command', 'uuid'), ('ad_hoc_command', 'end_line'), ('ad_hoc_command', 'start_line')]), + ), + migrations.AlterIndexTogether( + name='jobevent', + index_together=set([('job', 'event'), ('job', 'parent'), ('job', 'start_line'), ('job', 'uuid'), ('job', 'end_line')]), + ), + # Tower state + migrations.CreateModel( + name='TowerScheduleState', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('schedule_last_run', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'abstract': False, + }, + ), + # Tower instance capacity + migrations.AddField( + model_name='instance', + name='capacity', + field=models.PositiveIntegerField(default=100, editable=False), + ), + # Workflow surveys + migrations.AddField( + model_name='workflowjob', + name='survey_passwords', + field=jsonfield.fields.JSONField(default={}, editable=False, blank=True), + ), + migrations.AddField( + model_name='workflowjobtemplate', + name='survey_enabled', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='workflowjobtemplate', + name='survey_spec', + field=jsonfield.fields.JSONField(default={}, blank=True), + ), + # JSON field changes + migrations.AlterField( + model_name='adhoccommandevent', + name='event_data', + field=awx.main.fields.JSONField(default={}, blank=True), + ), + migrations.AlterField( + model_name='job', + name='artifacts', + field=awx.main.fields.JSONField(default={}, editable=False, blank=True), + ), + migrations.AlterField( + model_name='job', + name='survey_passwords', + field=awx.main.fields.JSONField(default={}, editable=False, blank=True), + ), + migrations.AlterField( + model_name='jobevent', + name='event_data', + field=awx.main.fields.JSONField(default={}, blank=True), + ), + migrations.AlterField( + model_name='jobtemplate', + name='survey_spec', + field=awx.main.fields.JSONField(default={}, blank=True), + ), + migrations.AlterField( + model_name='notification', + name='body', + field=awx.main.fields.JSONField(default=dict, blank=True), + ), + migrations.AlterField( + model_name='notificationtemplate', + name='notification_configuration', + field=awx.main.fields.JSONField(default=dict), + ), + migrations.AlterField( + model_name='project', + name='playbook_files', + field=awx.main.fields.JSONField(default=[], help_text='List of playbooks found in the project', verbose_name='Playbook Files', editable=False, blank=True), + ), + migrations.AlterField( + model_name='schedule', + name='extra_data', + field=awx.main.fields.JSONField(default={}, blank=True), + ), + migrations.AlterField( + model_name='unifiedjob', + name='job_env', + field=awx.main.fields.JSONField(default={}, editable=False, blank=True), + ), + migrations.AlterField( + model_name='workflowjob', + name='survey_passwords', + field=awx.main.fields.JSONField(default={}, editable=False, blank=True), + ), + migrations.AlterField( + model_name='workflowjobnode', + name='ancestor_artifacts', + field=awx.main.fields.JSONField(default={}, editable=False, blank=True), + ), + migrations.AlterField( + model_name='workflowjobnode', + name='char_prompts', + field=awx.main.fields.JSONField(default={}, blank=True), + ), + migrations.AlterField( + model_name='workflowjobtemplate', + name='survey_spec', + field=awx.main.fields.JSONField(default={}, blank=True), + ), + migrations.AlterField( + model_name='workflowjobtemplatenode', + name='char_prompts', + field=awx.main.fields.JSONField(default={}, blank=True), + ), + # Job Project Update + migrations.AddField( + model_name='job', + name='project_update', + field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.ProjectUpdate', help_text='The SCM Refresh task used to make sure the playbooks were available for the job run', null=True), + ), + # Inventory, non-unique name + migrations.AlterField( + model_name='inventory', + name='name', + field=models.CharField(max_length=512), + ), + # Text and has schedules + migrations.RemoveField( + model_name='unifiedjobtemplate', + name='has_schedules', + ), + migrations.AlterField( + model_name='host', + name='instance_id', + field=models.CharField(default=b'', help_text='The value used by the remote inventory source to uniquely identify the host', max_length=1024, blank=True), + ), + migrations.AlterField( + model_name='project', + name='scm_clean', + field=models.BooleanField(default=False, help_text='Discard any local changes before syncing the project.'), + ), + migrations.AlterField( + model_name='project', + name='scm_delete_on_update', + field=models.BooleanField(default=False, help_text='Delete the project before syncing.'), + ), + migrations.AlterField( + model_name='project', + name='scm_type', + field=models.CharField(default=b'', choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion')], max_length=8, blank=True, help_text='Specifies the source control system used to store the project.', verbose_name='SCM Type'), + ), + migrations.AlterField( + model_name='project', + name='scm_update_cache_timeout', + field=models.PositiveIntegerField(default=0, help_text='The number of seconds after the last project update ran that a newproject update will be launched as a job dependency.', blank=True), + ), + migrations.AlterField( + model_name='project', + name='scm_update_on_launch', + field=models.BooleanField(default=False, help_text='Update the project when a job is launched that uses the project.'), + ), + migrations.AlterField( + model_name='project', + name='scm_url', + field=models.CharField(default=b'', help_text='The location where the project is stored.', max_length=1024, verbose_name='SCM URL', blank=True), + ), + migrations.AlterField( + model_name='project', + name='timeout', + field=models.IntegerField(default=0, help_text='The amount of time to run before the task is canceled.', blank=True), + ), + migrations.AlterField( + model_name='projectupdate', + name='scm_clean', + field=models.BooleanField(default=False, help_text='Discard any local changes before syncing the project.'), + ), + migrations.AlterField( + model_name='projectupdate', + name='scm_delete_on_update', + field=models.BooleanField(default=False, help_text='Delete the project before syncing.'), + ), + migrations.AlterField( + model_name='projectupdate', + name='scm_type', + field=models.CharField(default=b'', choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion')], max_length=8, blank=True, help_text='Specifies the source control system used to store the project.', verbose_name='SCM Type'), + ), + migrations.AlterField( + model_name='projectupdate', + name='scm_url', + field=models.CharField(default=b'', help_text='The location where the project is stored.', max_length=1024, verbose_name='SCM URL', blank=True), + ), + migrations.AlterField( + model_name='projectupdate', + name='timeout', + field=models.IntegerField(default=0, help_text='The amount of time to run before the task is canceled.', blank=True), + ), + migrations.AlterField( + model_name='schedule', + name='dtend', + field=models.DateTimeField(default=None, help_text='The last occurrence of the schedule occurs before this time, aftewards the schedule expires.', null=True, editable=False), + ), + migrations.AlterField( + model_name='schedule', + name='dtstart', + field=models.DateTimeField(default=None, help_text='The first occurrence of the schedule occurs on or after this time.', null=True, editable=False), + ), + migrations.AlterField( + model_name='schedule', + name='enabled', + field=models.BooleanField(default=True, help_text='Enables processing of this schedule by Tower.'), + ), + migrations.AlterField( + model_name='schedule', + name='next_run', + field=models.DateTimeField(default=None, help_text='The next time that the scheduled action will run.', null=True, editable=False), + ), + migrations.AlterField( + model_name='schedule', + name='rrule', + field=models.CharField(help_text='A value representing the schedules iCal recurrence rule.', max_length=255), + ), + migrations.AlterField( + model_name='unifiedjob', + name='elapsed', + field=models.DecimalField(help_text='Elapsed time in seconds that the job ran.', editable=False, max_digits=12, decimal_places=3), + ), + migrations.AlterField( + model_name='unifiedjob', + name='execution_node', + field=models.TextField(default=b'', help_text='The Tower node the job executed on.', editable=False, blank=True), + ), + migrations.AlterField( + model_name='unifiedjob', + name='finished', + field=models.DateTimeField(default=None, help_text='The date and time the job finished execution.', null=True, editable=False), + ), + migrations.AlterField( + model_name='unifiedjob', + name='job_explanation', + field=models.TextField(default=b'', help_text="A status field to indicate the state of the job if it wasn't able to run and capture stdout", editable=False, blank=True), + ), + migrations.AlterField( + model_name='unifiedjob', + name='started', + field=models.DateTimeField(default=None, help_text='The date and time the job was queued for starting.', null=True, editable=False), + ), + + ] diff --git a/awx/main/migrations/0035_v310_modify_ha_instance.py b/awx/main/migrations/0035_v310_modify_ha_instance.py deleted file mode 100644 index fa58ec094c..0000000000 --- a/awx/main/migrations/0035_v310_modify_ha_instance.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0034_v310_add_workflows'), - ] - - operations = [ - migrations.RemoveField( - model_name='instance', - name='primary', - ), - migrations.AlterField( - model_name='instance', - name='uuid', - field=models.CharField(max_length=40), - ), - ] diff --git a/awx/main/migrations/0036_v310_jobevent_uuid.py b/awx/main/migrations/0036_v310_jobevent_uuid.py deleted file mode 100644 index b097c2d1f1..0000000000 --- a/awx/main/migrations/0036_v310_jobevent_uuid.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0035_v310_modify_ha_instance'), - ] - - operations = [ - migrations.AddField( - model_name='jobevent', - name='uuid', - field=models.CharField(default=b'', max_length=1024, editable=False), - ), - ] diff --git a/awx/main/migrations/0037_v310_remove_tower_settings.py b/awx/main/migrations/0037_v310_remove_tower_settings.py deleted file mode 100644 index 00ee17f098..0000000000 --- a/awx/main/migrations/0037_v310_remove_tower_settings.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0036_v310_jobevent_uuid'), - ] - - # These settings are now in the separate awx.conf app. - operations = [ - migrations.RemoveField( - model_name='towersettings', - name='user', - ), - migrations.DeleteModel( - name='TowerSettings', - ), - ] diff --git a/awx/main/migrations/0038_v310_job_allow_simultaneous.py b/awx/main/migrations/0038_v310_job_allow_simultaneous.py deleted file mode 100644 index 1ec3412fb4..0000000000 --- a/awx/main/migrations/0038_v310_job_allow_simultaneous.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0037_v310_remove_tower_settings'), - ] - - operations = [ - migrations.AddField( - model_name='job', - name='allow_simultaneous', - field=models.BooleanField(default=False), - ), - ] diff --git a/awx/main/migrations/0039_v310_workflow_rbac_prompts.py b/awx/main/migrations/0039_v310_workflow_rbac_prompts.py deleted file mode 100644 index 35db9c5575..0000000000 --- a/awx/main/migrations/0039_v310_workflow_rbac_prompts.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import jsonfield.fields -import django.db.models.deletion -import awx.main.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0038_v310_job_allow_simultaneous'), - ] - - operations = [ - migrations.AddField( - model_name='workflowjobnode', - name='char_prompts', - field=jsonfield.fields.JSONField(default={}, blank=True), - ), - migrations.AddField( - model_name='workflowjobnode', - name='credential', - field=models.ForeignKey(related_name='workflowjobnodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True), - ), - migrations.AddField( - model_name='workflowjobnode', - name='inventory', - field=models.ForeignKey(related_name='workflowjobnodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Inventory', null=True), - ), - migrations.AddField( - model_name='workflowjobtemplate', - name='execute_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'), - ), - migrations.AddField( - model_name='workflowjobtemplate', - name='organization', - field=models.ForeignKey(related_name='workflows', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='main.Organization', null=True), - ), - migrations.AddField( - model_name='workflowjobtemplate', - name='read_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'singleton:system_auditor', b'organization.auditor_role', b'execute_role', b'admin_role'], to='main.Role', null=b'True'), - ), - migrations.AddField( - model_name='workflowjobtemplatenode', - name='char_prompts', - field=jsonfield.fields.JSONField(default={}, blank=True), - ), - migrations.AddField( - model_name='workflowjobtemplatenode', - name='credential', - field=models.ForeignKey(related_name='workflowjobtemplatenodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True), - ), - migrations.AddField( - model_name='workflowjobtemplatenode', - name='inventory', - field=models.ForeignKey(related_name='workflowjobtemplatenodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Inventory', null=True), - ), - migrations.AlterField( - model_name='workflowjobnode', - name='unified_job_template', - field=models.ForeignKey(related_name='workflowjobnodes', on_delete=django.db.models.deletion.SET_NULL, default=None, to='main.UnifiedJobTemplate', null=True), - ), - migrations.AlterField( - model_name='workflowjobnode', - name='workflow_job', - field=models.ForeignKey(related_name='workflow_job_nodes', default=None, blank=True, to='main.WorkflowJob', null=True), - ), - migrations.AlterField( - model_name='workflowjobtemplate', - name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'singleton:system_administrator', b'organization.admin_role'], to='main.Role', null=b'True'), - ), - migrations.AlterField( - model_name='workflowjobtemplatenode', - name='unified_job_template', - field=models.ForeignKey(related_name='workflowjobtemplatenodes', on_delete=django.db.models.deletion.SET_NULL, default=None, to='main.UnifiedJobTemplate', null=True), - ), - ] diff --git a/awx/main/migrations/0040_v310_channelgroup.py b/awx/main/migrations/0040_v310_channelgroup.py deleted file mode 100644 index 51f2016926..0000000000 --- a/awx/main/migrations/0040_v310_channelgroup.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0039_v310_workflow_rbac_prompts'), - ] - - operations = [ - migrations.CreateModel( - name='ChannelGroup', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('group', models.CharField(unique=True, max_length=200)), - ('channels', models.TextField()), - ], - ), - ] diff --git a/awx/main/migrations/0041_v310_artifacts.py b/awx/main/migrations/0041_v310_artifacts.py deleted file mode 100644 index f54cff411b..0000000000 --- a/awx/main/migrations/0041_v310_artifacts.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import jsonfield.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0040_v310_channelgroup'), - ] - - operations = [ - migrations.AddField( - model_name='job', - name='artifacts', - field=jsonfield.fields.JSONField(default={}, editable=False, blank=True), - ), - migrations.AddField( - model_name='workflowjobnode', - name='ancestor_artifacts', - field=jsonfield.fields.JSONField(default={}, editable=False, blank=True), - ), - ] diff --git a/awx/main/migrations/0042_v310_job_timeout.py b/awx/main/migrations/0042_v310_job_timeout.py deleted file mode 100644 index 4d49e5841a..0000000000 --- a/awx/main/migrations/0042_v310_job_timeout.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0041_v310_artifacts'), - ] - - operations = [ - migrations.AddField( - model_name='inventorysource', - name='timeout', - field=models.PositiveIntegerField(default=0, blank=True), - ), - migrations.AddField( - model_name='inventoryupdate', - name='timeout', - field=models.PositiveIntegerField(default=0, blank=True), - ), - migrations.AddField( - model_name='job', - name='timeout', - field=models.PositiveIntegerField(default=0, blank=True), - ), - migrations.AddField( - model_name='jobtemplate', - name='timeout', - field=models.PositiveIntegerField(default=0, blank=True), - ), - migrations.AddField( - model_name='project', - name='timeout', - field=models.PositiveIntegerField(default=0, blank=True), - ), - migrations.AddField( - model_name='projectupdate', - name='timeout', - field=models.PositiveIntegerField(default=0, blank=True), - ), - ] diff --git a/awx/main/migrations/0043_v310_executionnode.py b/awx/main/migrations/0043_v310_executionnode.py deleted file mode 100644 index bab47ad032..0000000000 --- a/awx/main/migrations/0043_v310_executionnode.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0042_v310_job_timeout'), - ] - - operations = [ - migrations.AddField( - model_name='unifiedjob', - name='execution_node', - field=models.TextField(default=b'', editable=False, blank=True), - ), - ] diff --git a/awx/main/migrations/0044_v310_scm_revision.py b/awx/main/migrations/0044_v310_scm_revision.py deleted file mode 100644 index 40ee1f8596..0000000000 --- a/awx/main/migrations/0044_v310_scm_revision.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0043_v310_executionnode'), - ] - - operations = [ - migrations.AddField( - model_name='project', - name='scm_revision', - field=models.CharField(default=b'', editable=False, max_length=1024, blank=True, help_text='The last revision fetched by a project update', verbose_name='SCM Revision'), - ), - migrations.AddField( - model_name='projectupdate', - name='job_type', - field=models.CharField(default=b'check', max_length=64, choices=[(b'run', 'Run'), (b'check', 'Check')]), - ), - migrations.AddField( - model_name='job', - name='scm_revision', - field=models.CharField(default=b'', editable=False, max_length=1024, blank=True, help_text='The SCM Revision from the Project used for this job, if available', verbose_name='SCM Revision'), - ), - - ] diff --git a/awx/main/migrations/0045_v310_project_playbook_files.py b/awx/main/migrations/0045_v310_project_playbook_files.py deleted file mode 100644 index 77c7bfc8b7..0000000000 --- a/awx/main/migrations/0045_v310_project_playbook_files.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import jsonfield.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0044_v310_scm_revision'), - ] - - operations = [ - migrations.AddField( - model_name='project', - name='playbook_files', - field=jsonfield.fields.JSONField(default=[], help_text='List of playbooks found in the project', verbose_name='Playbook Files', editable=False, blank=True), - ), - ] diff --git a/awx/main/migrations/0046_v310_job_event_stdout.py b/awx/main/migrations/0046_v310_job_event_stdout.py deleted file mode 100644 index 7ff2ed8ade..0000000000 --- a/awx/main/migrations/0046_v310_job_event_stdout.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0045_v310_project_playbook_files'), - ] - - operations = [ - migrations.AddField( - model_name='adhoccommandevent', - name='end_line', - field=models.PositiveIntegerField(default=0, editable=False), - ), - migrations.AddField( - model_name='adhoccommandevent', - name='start_line', - field=models.PositiveIntegerField(default=0, editable=False), - ), - migrations.AddField( - model_name='adhoccommandevent', - name='stdout', - field=models.TextField(default=b'', editable=False), - ), - migrations.AddField( - model_name='adhoccommandevent', - name='uuid', - field=models.CharField(default=b'', max_length=1024, editable=False), - ), - migrations.AddField( - model_name='adhoccommandevent', - name='verbosity', - field=models.PositiveIntegerField(default=0, editable=False), - ), - migrations.AddField( - model_name='jobevent', - name='end_line', - field=models.PositiveIntegerField(default=0, editable=False), - ), - migrations.AddField( - model_name='jobevent', - name='playbook', - field=models.CharField(default=b'', max_length=1024, editable=False), - ), - migrations.AddField( - model_name='jobevent', - name='start_line', - field=models.PositiveIntegerField(default=0, editable=False), - ), - migrations.AddField( - model_name='jobevent', - name='stdout', - field=models.TextField(default=b'', editable=False), - ), - migrations.AddField( - model_name='jobevent', - name='verbosity', - field=models.PositiveIntegerField(default=0, editable=False), - ), - migrations.AlterField( - model_name='adhoccommandevent', - name='counter', - field=models.PositiveIntegerField(default=0, editable=False), - ), - migrations.AlterField( - model_name='adhoccommandevent', - name='event', - field=models.CharField(max_length=100, choices=[(b'runner_on_failed', 'Host Failed'), (b'runner_on_ok', 'Host OK'), (b'runner_on_unreachable', 'Host Unreachable'), (b'runner_on_skipped', 'Host Skipped'), (b'debug', 'Debug'), (b'verbose', 'Verbose'), (b'deprecated', 'Deprecated'), (b'warning', 'Warning'), (b'system_warning', 'System Warning'), (b'error', 'Error')]), - ), - migrations.AlterField( - model_name='jobevent', - name='counter', - field=models.PositiveIntegerField(default=0, editable=False), - ), - migrations.AlterField( - model_name='jobevent', - name='event', - field=models.CharField(max_length=100, choices=[(b'runner_on_failed', 'Host Failed'), (b'runner_on_ok', 'Host OK'), (b'runner_on_error', 'Host Failure'), (b'runner_on_skipped', 'Host Skipped'), (b'runner_on_unreachable', 'Host Unreachable'), (b'runner_on_no_hosts', 'No Hosts Remaining'), (b'runner_on_async_poll', 'Host Polling'), (b'runner_on_async_ok', 'Host Async OK'), (b'runner_on_async_failed', 'Host Async Failure'), (b'runner_item_on_ok', 'Item OK'), (b'runner_item_on_failed', 'Item Failed'), (b'runner_item_on_skipped', 'Item Skipped'), (b'runner_retry', 'Host Retry'), (b'runner_on_file_diff', 'File Difference'), (b'playbook_on_start', 'Playbook Started'), (b'playbook_on_notify', 'Running Handlers'), (b'playbook_on_include', 'Including File'), (b'playbook_on_no_hosts_matched', 'No Hosts Matched'), (b'playbook_on_no_hosts_remaining', 'No Hosts Remaining'), (b'playbook_on_task_start', 'Task Started'), (b'playbook_on_vars_prompt', 'Variables Prompted'), (b'playbook_on_setup', 'Gathering Facts'), (b'playbook_on_import_for_host', 'internal: on Import for Host'), (b'playbook_on_not_import_for_host', 'internal: on Not Import for Host'), (b'playbook_on_play_start', 'Play Started'), (b'playbook_on_stats', 'Playbook Complete'), (b'debug', 'Debug'), (b'verbose', 'Verbose'), (b'deprecated', 'Deprecated'), (b'warning', 'Warning'), (b'system_warning', 'System Warning'), (b'error', 'Error')]), - ), - migrations.AlterUniqueTogether( - name='adhoccommandevent', - unique_together=set([]), - ), - migrations.AlterIndexTogether( - name='adhoccommandevent', - index_together=set([('ad_hoc_command', 'event'), ('ad_hoc_command', 'uuid'), ('ad_hoc_command', 'end_line'), ('ad_hoc_command', 'start_line')]), - ), - migrations.AlterIndexTogether( - name='jobevent', - index_together=set([('job', 'event'), ('job', 'parent'), ('job', 'start_line'), ('job', 'uuid'), ('job', 'end_line')]), - ), - ] diff --git a/awx/main/migrations/0047_v310_tower_state.py b/awx/main/migrations/0047_v310_tower_state.py deleted file mode 100644 index 941dfd0ba2..0000000000 --- a/awx/main/migrations/0047_v310_tower_state.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0046_v310_job_event_stdout'), - ] - - operations = [ - migrations.CreateModel( - name='TowerScheduleState', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('schedule_last_run', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/awx/main/migrations/0048_v310_instance_capacity.py b/awx/main/migrations/0048_v310_instance_capacity.py deleted file mode 100644 index 5f0795a4fd..0000000000 --- a/awx/main/migrations/0048_v310_instance_capacity.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0047_v310_tower_state'), - ] - - operations = [ - migrations.AddField( - model_name='instance', - name='capacity', - field=models.PositiveIntegerField(default=100, editable=False), - ), - ] diff --git a/awx/main/migrations/0049_v310_workflow_surveys.py b/awx/main/migrations/0049_v310_workflow_surveys.py deleted file mode 100644 index bc3865d33c..0000000000 --- a/awx/main/migrations/0049_v310_workflow_surveys.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import jsonfield.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0048_v310_instance_capacity'), - ] - - operations = [ - migrations.AddField( - model_name='workflowjob', - name='survey_passwords', - field=jsonfield.fields.JSONField(default={}, editable=False, blank=True), - ), - migrations.AddField( - model_name='workflowjobtemplate', - name='survey_enabled', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='workflowjobtemplate', - name='survey_spec', - field=jsonfield.fields.JSONField(default={}, blank=True), - ), - ] diff --git a/awx/main/migrations/0050_v310_JSONField_changes.py b/awx/main/migrations/0050_v310_JSONField_changes.py deleted file mode 100644 index b2f3f2534c..0000000000 --- a/awx/main/migrations/0050_v310_JSONField_changes.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import awx.main.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0049_v310_workflow_surveys'), - ] - - operations = [ - migrations.AlterField( - model_name='adhoccommandevent', - name='event_data', - field=awx.main.fields.JSONField(default={}, blank=True), - ), - migrations.AlterField( - model_name='job', - name='artifacts', - field=awx.main.fields.JSONField(default={}, editable=False, blank=True), - ), - migrations.AlterField( - model_name='job', - name='survey_passwords', - field=awx.main.fields.JSONField(default={}, editable=False, blank=True), - ), - migrations.AlterField( - model_name='jobevent', - name='event_data', - field=awx.main.fields.JSONField(default={}, blank=True), - ), - migrations.AlterField( - model_name='jobtemplate', - name='survey_spec', - field=awx.main.fields.JSONField(default={}, blank=True), - ), - migrations.AlterField( - model_name='notification', - name='body', - field=awx.main.fields.JSONField(default=dict, blank=True), - ), - migrations.AlterField( - model_name='notificationtemplate', - name='notification_configuration', - field=awx.main.fields.JSONField(default=dict), - ), - migrations.AlterField( - model_name='project', - name='playbook_files', - field=awx.main.fields.JSONField(default=[], help_text='List of playbooks found in the project', verbose_name='Playbook Files', editable=False, blank=True), - ), - migrations.AlterField( - model_name='schedule', - name='extra_data', - field=awx.main.fields.JSONField(default={}, blank=True), - ), - migrations.AlterField( - model_name='unifiedjob', - name='job_env', - field=awx.main.fields.JSONField(default={}, editable=False, blank=True), - ), - migrations.AlterField( - model_name='workflowjob', - name='survey_passwords', - field=awx.main.fields.JSONField(default={}, editable=False, blank=True), - ), - migrations.AlterField( - model_name='workflowjobnode', - name='ancestor_artifacts', - field=awx.main.fields.JSONField(default={}, editable=False, blank=True), - ), - migrations.AlterField( - model_name='workflowjobnode', - name='char_prompts', - field=awx.main.fields.JSONField(default={}, blank=True), - ), - migrations.AlterField( - model_name='workflowjobtemplate', - name='survey_spec', - field=awx.main.fields.JSONField(default={}, blank=True), - ), - migrations.AlterField( - model_name='workflowjobtemplatenode', - name='char_prompts', - field=awx.main.fields.JSONField(default={}, blank=True), - ), - ] diff --git a/awx/main/migrations/0051_v310_job_project_update.py b/awx/main/migrations/0051_v310_job_project_update.py deleted file mode 100644 index 2732f6973d..0000000000 --- a/awx/main/migrations/0051_v310_job_project_update.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0050_v310_JSONField_changes'), - ] - - operations = [ - migrations.AddField( - model_name='job', - name='project_update', - field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.ProjectUpdate', help_text='The SCM Refresh task used to make sure the playbooks were available for the job run', null=True), - ), - ] diff --git a/awx/main/migrations/0052_v310_inventory_name_non_unique.py b/awx/main/migrations/0052_v310_inventory_name_non_unique.py deleted file mode 100644 index 83bf44619e..0000000000 --- a/awx/main/migrations/0052_v310_inventory_name_non_unique.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0051_v310_job_project_update'), - ] - - operations = [ - migrations.AlterField( - model_name='inventory', - name='name', - field=models.CharField(max_length=512), - ), - ] diff --git a/awx/main/migrations/0053_v310_update_timeout_field_type.py b/awx/main/migrations/0053_v310_update_timeout_field_type.py deleted file mode 100644 index 9365a4156a..0000000000 --- a/awx/main/migrations/0053_v310_update_timeout_field_type.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0052_v310_inventory_name_non_unique'), - ] - - operations = [ - migrations.AlterField( - model_name='inventorysource', - name='timeout', - field=models.IntegerField(default=0, blank=True), - ), - migrations.AlterField( - model_name='inventoryupdate', - name='timeout', - field=models.IntegerField(default=0, blank=True), - ), - migrations.AlterField( - model_name='job', - name='timeout', - field=models.IntegerField(default=0, blank=True), - ), - migrations.AlterField( - model_name='jobtemplate', - name='timeout', - field=models.IntegerField(default=0, blank=True), - ), - migrations.AlterField( - model_name='project', - name='timeout', - field=models.IntegerField(default=0, blank=True), - ), - migrations.AlterField( - model_name='projectupdate', - name='timeout', - field=models.IntegerField(default=0, blank=True), - ), - ] diff --git a/awx/main/migrations/0054_text_and_has_schedules.py b/awx/main/migrations/0054_text_and_has_schedules.py deleted file mode 100644 index 426e40f211..0000000000 --- a/awx/main/migrations/0054_text_and_has_schedules.py +++ /dev/null @@ -1,133 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0053_v310_update_timeout_field_type'), - ] - - operations = [ - migrations.RemoveField( - model_name='unifiedjobtemplate', - name='has_schedules', - ), - migrations.AlterField( - model_name='host', - name='instance_id', - field=models.CharField(default=b'', help_text='The value used by the remote inventory source to uniquely identify the host', max_length=1024, blank=True), - ), - migrations.AlterField( - model_name='project', - name='scm_clean', - field=models.BooleanField(default=False, help_text='Discard any local changes before syncing the project.'), - ), - migrations.AlterField( - model_name='project', - name='scm_delete_on_update', - field=models.BooleanField(default=False, help_text='Delete the project before syncing.'), - ), - migrations.AlterField( - model_name='project', - name='scm_type', - field=models.CharField(default=b'', choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion')], max_length=8, blank=True, help_text='Specifies the source control system used to store the project.', verbose_name='SCM Type'), - ), - migrations.AlterField( - model_name='project', - name='scm_update_cache_timeout', - field=models.PositiveIntegerField(default=0, help_text='The number of seconds after the last project update ran that a newproject update will be launched as a job dependency.', blank=True), - ), - migrations.AlterField( - model_name='project', - name='scm_update_on_launch', - field=models.BooleanField(default=False, help_text='Update the project when a job is launched that uses the project.'), - ), - migrations.AlterField( - model_name='project', - name='scm_url', - field=models.CharField(default=b'', help_text='The location where the project is stored.', max_length=1024, verbose_name='SCM URL', blank=True), - ), - migrations.AlterField( - model_name='project', - name='timeout', - field=models.IntegerField(default=0, help_text='The amount of time to run before the task is canceled.', blank=True), - ), - migrations.AlterField( - model_name='projectupdate', - name='scm_clean', - field=models.BooleanField(default=False, help_text='Discard any local changes before syncing the project.'), - ), - migrations.AlterField( - model_name='projectupdate', - name='scm_delete_on_update', - field=models.BooleanField(default=False, help_text='Delete the project before syncing.'), - ), - migrations.AlterField( - model_name='projectupdate', - name='scm_type', - field=models.CharField(default=b'', choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion')], max_length=8, blank=True, help_text='Specifies the source control system used to store the project.', verbose_name='SCM Type'), - ), - migrations.AlterField( - model_name='projectupdate', - name='scm_url', - field=models.CharField(default=b'', help_text='The location where the project is stored.', max_length=1024, verbose_name='SCM URL', blank=True), - ), - migrations.AlterField( - model_name='projectupdate', - name='timeout', - field=models.IntegerField(default=0, help_text='The amount of time to run before the task is canceled.', blank=True), - ), - migrations.AlterField( - model_name='schedule', - name='dtend', - field=models.DateTimeField(default=None, help_text='The last occurrence of the schedule occurs before this time, aftewards the schedule expires.', null=True, editable=False), - ), - migrations.AlterField( - model_name='schedule', - name='dtstart', - field=models.DateTimeField(default=None, help_text='The first occurrence of the schedule occurs on or after this time.', null=True, editable=False), - ), - migrations.AlterField( - model_name='schedule', - name='enabled', - field=models.BooleanField(default=True, help_text='Enables processing of this schedule by Tower.'), - ), - migrations.AlterField( - model_name='schedule', - name='next_run', - field=models.DateTimeField(default=None, help_text='The next time that the scheduled action will run.', null=True, editable=False), - ), - migrations.AlterField( - model_name='schedule', - name='rrule', - field=models.CharField(help_text='A value representing the schedules iCal recurrence rule.', max_length=255), - ), - migrations.AlterField( - model_name='unifiedjob', - name='elapsed', - field=models.DecimalField(help_text='Elapsed time in seconds that the job ran.', editable=False, max_digits=12, decimal_places=3), - ), - migrations.AlterField( - model_name='unifiedjob', - name='execution_node', - field=models.TextField(default=b'', help_text='The Tower node the job executed on.', editable=False, blank=True), - ), - migrations.AlterField( - model_name='unifiedjob', - name='finished', - field=models.DateTimeField(default=None, help_text='The date and time the job finished execution.', null=True, editable=False), - ), - migrations.AlterField( - model_name='unifiedjob', - name='job_explanation', - field=models.TextField(default=b'', help_text="A status field to indicate the state of the job if it wasn't able to run and capture stdout", editable=False, blank=True), - ), - migrations.AlterField( - model_name='unifiedjob', - name='started', - field=models.DateTimeField(default=None, help_text='The date and time the job was queued for starting.', null=True, editable=False), - ), - ] From 83b985ed2e1da780555a49ae350ad0b47b89a996 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 12 Dec 2016 15:12:16 -0500 Subject: [PATCH 106/595] tower settings removal depends on conf migration --- .../0002_v310_copy_tower_settings.py | 4 +-- awx/main/migrations/0034_v310_release.py | 8 ------ .../0035_v310_remove_tower_settings.py | 27 +++++++++++++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 awx/main/migrations/0035_v310_remove_tower_settings.py diff --git a/awx/conf/migrations/0002_v310_copy_tower_settings.py b/awx/conf/migrations/0002_v310_copy_tower_settings.py index 4493007f70..7cf24b7061 100644 --- a/awx/conf/migrations/0002_v310_copy_tower_settings.py +++ b/awx/conf/migrations/0002_v310_copy_tower_settings.py @@ -64,11 +64,11 @@ class Migration(migrations.Migration): dependencies = [ ('conf', '0001_initial'), - ('main', '0036_v310_jobevent_uuid'), + ('main', '0034_v310_release'), ] run_before = [ - ('main', '0037_v310_remove_tower_settings'), + ('main', '0035_v310_remove_tower_settings'), ] operations = [ diff --git a/awx/main/migrations/0034_v310_release.py b/awx/main/migrations/0034_v310_release.py index e86691d2e1..480f5dda06 100644 --- a/awx/main/migrations/0034_v310_release.py +++ b/awx/main/migrations/0034_v310_release.py @@ -31,14 +31,6 @@ class Migration(migrations.Migration): name='allow_simultaneous', field=models.BooleanField(default=False), ), - # Remove Tower settings, these settings are now in separate awx.conf app. - migrations.RemoveField( - model_name='towersettings', - name='user', - ), - migrations.DeleteModel( - name='TowerSettings', - ), # Job Event UUID migrations.AddField( model_name='jobevent', diff --git a/awx/main/migrations/0035_v310_remove_tower_settings.py b/awx/main/migrations/0035_v310_remove_tower_settings.py new file mode 100644 index 0000000000..bdb0d284fa --- /dev/null +++ b/awx/main/migrations/0035_v310_remove_tower_settings.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import awx.main.models.notifications +import jsonfield.fields +import django.db.models.deletion +import awx.main.models.workflow +import awx.main.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0034_v310_release'), + ] + + operations = [ + # Remove Tower settings, these settings are now in separate awx.conf app. + migrations.RemoveField( + model_name='towersettings', + name='user', + ), + migrations.DeleteModel( + name='TowerSettings', + ), + ] From c9dab5805d1fa4ed407312286c55a74ca344534e Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 12 Dec 2016 19:22:09 -0500 Subject: [PATCH 107/595] update imports and further consolidate --- awx/main/migrations/0034_v310_release.py | 2 +- awx/main/migrations/0035_v310_remove_tower_settings.py | 7 +------ awx/main/models/workflow.py | 5 ----- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/awx/main/migrations/0034_v310_release.py b/awx/main/migrations/0034_v310_release.py index 480f5dda06..fa46beec20 100644 --- a/awx/main/migrations/0034_v310_release.py +++ b/awx/main/migrations/0034_v310_release.py @@ -62,7 +62,7 @@ class Migration(migrations.Migration): options={ 'ordering': ('id',), }, - bases=('main.unifiedjob', models.Model, awx.main.models.notifications.JobNotificationMixin, awx.main.models.workflow.WorkflowJobInheritNodesMixin), + bases=('main.unifiedjob', models.Model, awx.main.models.notifications.JobNotificationMixin), ), migrations.CreateModel( name='WorkflowJobNode', diff --git a/awx/main/migrations/0035_v310_remove_tower_settings.py b/awx/main/migrations/0035_v310_remove_tower_settings.py index bdb0d284fa..e92dfe605c 100644 --- a/awx/main/migrations/0035_v310_remove_tower_settings.py +++ b/awx/main/migrations/0035_v310_remove_tower_settings.py @@ -1,12 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models -import awx.main.models.notifications -import jsonfield.fields -import django.db.models.deletion -import awx.main.models.workflow -import awx.main.fields +from django.db import migrations class Migration(migrations.Migration): diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 1cd4d44348..fcf2749055 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -435,11 +435,6 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl return new_wfjt -# Stub in place because of old migrations, can remove if migrations are squashed -class WorkflowJobInheritNodesMixin(object): - pass - - class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin): class Meta: app_label = 'main' From 074fe8a3d10515b5b70b76d91fda0dc882b8a7df Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 12 Dec 2016 16:33:37 -0800 Subject: [PATCH 108/595] adding some fields to the job results left hand side panel --- .../src/job-results/job-results.controller.js | 3 +- .../src/job-results/job-results.partial.html | 104 +++++------------- 2 files changed, 32 insertions(+), 75 deletions(-) diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index b2f83ede02..ce92fb95f7 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -31,6 +31,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' $scope.machine_credential_link = getTowerLink('credential'); $scope.cloud_credential_link = getTowerLink('cloud_credential'); $scope.network_credential_link = getTowerLink('network_credential'); + $scope.schedule_link = getTowerLink('schedule'); }; // uses options to set scope variables to their readable string @@ -64,7 +65,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' // turn related api browser routes into tower routes getTowerLinks(); - + // the links below can't be set in getTowerLinks because the // links on the UI don't directly match the corresponding URL // on the API browser diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index b12c7af487..85e4bfedf0 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -62,6 +62,20 @@
+ +
+ +
+ + {{ status_label }} +
+
+
@@ -135,6 +149,22 @@
+ + +
@@ -348,80 +378,6 @@
- - - - - - - - - - - -
- -
- -
- - {{ status_label }} -
-
-
@@ -154,7 +140,7 @@ ng-show="job.summary_fields.schedule.name">
+ fa icon-job-{{ job.status }}" + aw-tool-tip="Job {{status_label}}" + aw-tip-placement="top" + data-original-title> {{ job.name }}
From 384dbb6da2254af3ce4f2387aded5a9fb601ac97 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Tue, 13 Dec 2016 14:41:23 -0500 Subject: [PATCH 116/595] renamed squashed migrations to start at 0002 --- ...1_squashed_v300_release.py => 0002_squashed_v300_release.py} | 0 ..._v300_v303_updates.py => 0003_squashed_v300_v303_updates.py} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename awx/main/migrations/{0001_squashed_v300_release.py => 0002_squashed_v300_release.py} (100%) rename awx/main/migrations/{0002_squashed_v300_v303_updates.py => 0003_squashed_v300_v303_updates.py} (99%) diff --git a/awx/main/migrations/0001_squashed_v300_release.py b/awx/main/migrations/0002_squashed_v300_release.py similarity index 100% rename from awx/main/migrations/0001_squashed_v300_release.py rename to awx/main/migrations/0002_squashed_v300_release.py diff --git a/awx/main/migrations/0002_squashed_v300_v303_updates.py b/awx/main/migrations/0003_squashed_v300_v303_updates.py similarity index 99% rename from awx/main/migrations/0002_squashed_v300_v303_updates.py rename to awx/main/migrations/0003_squashed_v300_v303_updates.py index 223e307089..837a9e75ee 100644 --- a/awx/main/migrations/0002_squashed_v300_v303_updates.py +++ b/awx/main/migrations/0003_squashed_v300_v303_updates.py @@ -40,7 +40,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('main', '0001_squashed_v300_release'), + ('main', '0002_squashed_v300_release'), ] operations = [ From 434b0f1f25fb0fff99f6d366ee60d31f85cfb817 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Tue, 13 Dec 2016 14:42:24 -0500 Subject: [PATCH 117/595] remove unneeded data migrations from squashed migrations --- awx/main/migrations/0002_squashed_v300_release.py | 3 --- awx/main/migrations/0003_squashed_v300_v303_updates.py | 6 ------ 2 files changed, 9 deletions(-) diff --git a/awx/main/migrations/0002_squashed_v300_release.py b/awx/main/migrations/0002_squashed_v300_release.py index c2244f69eb..c398d18468 100644 --- a/awx/main/migrations/0002_squashed_v300_release.py +++ b/awx/main/migrations/0002_squashed_v300_release.py @@ -6,7 +6,6 @@ from __future__ import unicode_literals import awx.main.fields -from awx.main.migrations import _cleanup_deleted as cleanup_deleted from django.db import migrations, models import django.db.models.deletion @@ -248,8 +247,6 @@ class Migration(migrations.Migration): name='fact', index_together=set([('timestamp', 'module', 'host')]), ), - # Active flag cleanup - migrations.RunPython(cleanup_deleted.cleanup_deleted), # Active flag removal migrations.RemoveField( model_name='credential', diff --git a/awx/main/migrations/0003_squashed_v300_v303_updates.py b/awx/main/migrations/0003_squashed_v300_v303_updates.py index 837a9e75ee..82d781ec85 100644 --- a/awx/main/migrations/0003_squashed_v300_v303_updates.py +++ b/awx/main/migrations/0003_squashed_v300_v303_updates.py @@ -5,9 +5,6 @@ from __future__ import unicode_literals -from awx.main.migrations import _migration_utils as migration_utils -from awx.main.migrations import _save_password_keys - from django.db import migrations, models from django.conf import settings import awx.main.fields @@ -44,7 +41,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(migration_utils.set_current_apps_for_migrations), # Labels Changes migrations.RemoveField( model_name='job', @@ -146,7 +142,6 @@ class Migration(migrations.Migration): name='survey_passwords', field=jsonfield.fields.JSONField(default={}, editable=False, blank=True), ), - migrations.RunPython(_save_password_keys.migrate_survey_passwords), # RBAC credential permission updates migrations.AlterField( model_name='credential', @@ -158,5 +153,4 @@ class Migration(migrations.Migration): name='use_role', field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'), ), - migrations.RunPython(update_dashed_host_variables), ] From 7e87f5031af1fed5f74e71f91ebc6f3c969ab203 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 13 Dec 2016 15:13:13 -0500 Subject: [PATCH 118/595] Add 20px margin to the base workflow graph --- .../workflow-chart/workflow-chart.directive.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index cb6eeafa28..dfe17e4130 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -22,7 +22,7 @@ export default [ '$state', let margin = {top: 20, right: 20, bottom: 20, left: 20}, width = 950, - height = 590 - margin.top - margin.bottom, + height = 550, i = 0, rectW = 120, rectH = 60, @@ -39,15 +39,15 @@ export default [ '$state', let zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]); let baseSvg = d3.select(element[0]).append("svg") - .attr("width", width) - .attr("height", height) + .attr("width", width - margin.right - margin.left) + .attr("height", height - margin.top - margin.bottom) .attr("class", "WorkflowChart-svg") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")") .call(zoomObj .on("zoom", naturalZoom) ); - let svgGroup = baseSvg.append("g"); + let svgGroup = baseSvg.append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); function lineData(d){ @@ -108,6 +108,8 @@ export default [ '$state', let scale = d3.event.scale, translation = d3.event.translate; + translation = [translation[0] + (margin.left*scale), translation[1] + (margin.top*scale)]; + svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")"); scope.workflowZoomed({ @@ -125,7 +127,7 @@ export default [ '$state', translateX = unscaledOffsetX*scale - ((scale*width)-width)/2, translateY = unscaledOffsetY*scale - ((scale*height)-height)/2; - svgGroup.attr("transform", "translate(" + [translateX, translateY] + ")scale(" + scale + ")"); + svgGroup.attr("transform", "translate(" + [translateX + (margin.left*scale), translateY + (margin.top*scale)] + ")scale(" + scale + ")"); zoomObj.scale(scale); zoomObj.translate([translateX, translateY]); } @@ -148,7 +150,7 @@ export default [ '$state', } function resetZoomAndPan() { - svgGroup.attr("transform", "translate(" + 0 + "," + 0 + ")scale(" + 1 + ")"); + svgGroup.attr("transform", "translate(" + margin.left + "," + margin.top + ")scale(" + 1 + ")"); // Update the zoomObj zoomObj.scale(1); zoomObj.translate([0,0]); From f89bd15bf513d0855e5660a69c6c8f8f2fd7f01f Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 13 Dec 2016 15:27:32 -0500 Subject: [PATCH 119/595] Show started and finished on job summary fields --- awx/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 11d3c10234..66c963650e 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -81,7 +81,7 @@ SUMMARIZABLE_FK_FIELDS = { 'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'), 'cloud_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'), 'network_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'net'), - 'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), + 'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'started', 'finished'), 'job_template': DEFAULT_SUMMARY_FIELDS, 'schedule': DEFAULT_SUMMARY_FIELDS + ('next_run',), 'unified_job_template': DEFAULT_SUMMARY_FIELDS + ('unified_job_type',), From ec9065bc2f45d5c0022b6aeffa6a6bc4cb96ac57 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 13 Dec 2016 16:55:03 -0500 Subject: [PATCH 120/595] Added elapsed to the job default summary fields and removed started/finished --- awx/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 66c963650e..f6f7a3aae3 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -81,7 +81,7 @@ SUMMARIZABLE_FK_FIELDS = { 'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'), 'cloud_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'), 'network_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'net'), - 'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'started', 'finished'), + 'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed'), 'job_template': DEFAULT_SUMMARY_FIELDS, 'schedule': DEFAULT_SUMMARY_FIELDS + ('next_run',), 'unified_job_template': DEFAULT_SUMMARY_FIELDS + ('unified_job_type',), From 44c6127a48915e8f6a89d14763667925114630a6 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 13 Dec 2016 17:11:06 -0500 Subject: [PATCH 121/595] fix gce and maybe azure cert issues --- requirements/requirements.txt | 2 +- requirements/requirements_ansible.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 03a6d9f614..778e016da9 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -45,7 +45,7 @@ baron==0.6.2 # via redbaron billiard==3.3.0.23 # via celery boto==2.43.0 celery==3.1.17 -certifi==2016.9.26 # via msrest +#certifi==2016.9.26 # via msrest --- chris meyers removed this because it fails gce inv sync and azure "classic". By removing this, we coerce libcloud into using the cert file '/etc/pki/tls/certs/ca-bundle.crt ' cffi==1.9.1 # via cryptography channels==0.17.3 chardet==2.3.0 # via msrest diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index c3019c2dff..b1a669bc91 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -30,7 +30,7 @@ azure-storage==0.33.0 # via azure azure==2.0.0rc6 Babel==2.3.4 # via osc-lib, oslo.i18n, python-cinderclient, python-glanceclient, python-heatclient, python-magnumclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient boto==2.43.0 -certifi==2016.9.26 # via msrest +#certifi==2016.9.26 # via msrest --- chris meyers removed this because it fails gce inv sync and azure "classic". By removing this, we coerce libcloud into using the cert file '/etc/pki/tls/certs/ca-bundle.crt ' cffi==1.9.1 # via cryptography chardet==2.3.0 # via msrest cliff==2.3.0 # via osc-lib, python-designateclient, python-heatclient, python-mistralclient, python-neutronclient, python-openstackclient From 732eb6c6329c758571e58599808fb369e69d2970 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 13 Dec 2016 17:11:06 -0500 Subject: [PATCH 122/595] fix gce and maybe azure cert issues --- requirements/requirements.txt | 2 +- requirements/requirements_ansible.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 03a6d9f614..778e016da9 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -45,7 +45,7 @@ baron==0.6.2 # via redbaron billiard==3.3.0.23 # via celery boto==2.43.0 celery==3.1.17 -certifi==2016.9.26 # via msrest +#certifi==2016.9.26 # via msrest --- chris meyers removed this because it fails gce inv sync and azure "classic". By removing this, we coerce libcloud into using the cert file '/etc/pki/tls/certs/ca-bundle.crt ' cffi==1.9.1 # via cryptography channels==0.17.3 chardet==2.3.0 # via msrest diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index c3019c2dff..b1a669bc91 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -30,7 +30,7 @@ azure-storage==0.33.0 # via azure azure==2.0.0rc6 Babel==2.3.4 # via osc-lib, oslo.i18n, python-cinderclient, python-glanceclient, python-heatclient, python-magnumclient, python-neutronclient, python-novaclient, python-openstackclient, python-troveclient boto==2.43.0 -certifi==2016.9.26 # via msrest +#certifi==2016.9.26 # via msrest --- chris meyers removed this because it fails gce inv sync and azure "classic". By removing this, we coerce libcloud into using the cert file '/etc/pki/tls/certs/ca-bundle.crt ' cffi==1.9.1 # via cryptography chardet==2.3.0 # via msrest cliff==2.3.0 # via osc-lib, python-designateclient, python-heatclient, python-mistralclient, python-neutronclient, python-openstackclient From 248f03074422a12c27e5d62448545ba5039067dd Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 13 Dec 2016 17:14:43 -0500 Subject: [PATCH 123/595] bump dyn inv sync scripts --- awx/plugins/inventory/azure_rm.py | 8 +- awx/plugins/inventory/cloudforms.py | 3 +- awx/plugins/inventory/ec2.ini.example | 84 ++++++++- awx/plugins/inventory/ec2.py | 84 ++++++++- awx/plugins/inventory/gce.py | 236 +++++++++++++++++++++----- awx/plugins/inventory/openstack.py | 231 +++++++++++++++++-------- awx/plugins/inventory/openstack.yml | 4 + awx/plugins/inventory/vmware.py | 44 ++++- 8 files changed, 564 insertions(+), 130 deletions(-) diff --git a/awx/plugins/inventory/azure_rm.py b/awx/plugins/inventory/azure_rm.py index f3c9e7c28d..8545967c37 100755 --- a/awx/plugins/inventory/azure_rm.py +++ b/awx/plugins/inventory/azure_rm.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # # Copyright (c) 2016 Matt Davis, # Chris Houseknecht, @@ -786,11 +786,11 @@ class AzureInventory(object): def main(): if not HAS_AZURE: - sys.exit("The Azure python sdk is not installed (try 'pip install azure==2.0.0rc5') - {0}".format(HAS_AZURE_EXC)) + sys.exit("The Azure python sdk is not installed (try 'pip install azure>=2.0.0rc5') - {0}".format(HAS_AZURE_EXC)) - if LooseVersion(azure_compute_version) != LooseVersion(AZURE_MIN_VERSION): + if LooseVersion(azure_compute_version) < LooseVersion(AZURE_MIN_VERSION): sys.exit("Expecting azure.mgmt.compute.__version__ to be {0}. Found version {1} " - "Do you have Azure == 2.0.0rc5 installed?".format(AZURE_MIN_VERSION, azure_compute_version)) + "Do you have Azure >= 2.0.0rc5 installed?".format(AZURE_MIN_VERSION, azure_compute_version)) AzureInventory() diff --git a/awx/plugins/inventory/cloudforms.py b/awx/plugins/inventory/cloudforms.py index 65d95853d5..69c149bfc5 100755 --- a/awx/plugins/inventory/cloudforms.py +++ b/awx/plugins/inventory/cloudforms.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # vim: set fileencoding=utf-8 : # # Copyright (C) 2016 Guido Günther @@ -459,4 +459,3 @@ class CloudFormsInventory(object): return json.dumps(data) CloudFormsInventory() - diff --git a/awx/plugins/inventory/ec2.ini.example b/awx/plugins/inventory/ec2.ini.example index 1d7428b2ed..2b9f089135 100644 --- a/awx/plugins/inventory/ec2.ini.example +++ b/awx/plugins/inventory/ec2.ini.example @@ -29,23 +29,41 @@ regions_exclude = us-gov-west-1,cn-north-1 # in the event of a collision. destination_variable = public_dns_name +# This allows you to override the inventory_name with an ec2 variable, instead +# of using the destination_variable above. Addressing (aka ansible_ssh_host) +# will still use destination_variable. Tags should be written as 'tag_TAGNAME'. +#hostname_variable = tag_Name + # For server inside a VPC, using DNS names may not make sense. When an instance # has 'subnet_id' set, this variable is used. If the subnet is public, setting # this to 'ip_address' will return the public IP address. For instances in a # private subnet, this should be set to 'private_ip_address', and Ansible must # be run from within EC2. The key of an EC2 tag may optionally be used; however # the boto instance variables hold precedence in the event of a collision. -# WARNING: - instances that are in the private vpc, _without_ public ip address -# will not be listed in the inventory untill You set: -# vpc_destination_variable = 'private_ip_address' +# WARNING: - instances that are in the private vpc, _without_ public ip address +# will not be listed in the inventory until You set: +# vpc_destination_variable = private_ip_address vpc_destination_variable = ip_address +# The following two settings allow flexible ansible host naming based on a +# python format string and a comma-separated list of ec2 tags. Note that: +# +# 1) If the tags referenced are not present for some instances, empty strings +# will be substituted in the format string. +# 2) This overrides both destination_variable and vpc_destination_variable. +# +#destination_format = {0}.{1}.example.com +#destination_format_tags = Name,environment + # To tag instances on EC2 with the resource records that point to them from # Route53, uncomment and set 'route53' to True. route53 = False # To exclude RDS instances from the inventory, uncomment and set to False. -#rds = False +rds = False + +# To exclude ElastiCache instances from the inventory, uncomment and set to False. +elasticache = False # Additionally, you can specify the list of zones to exclude looking up in # 'route53_excluded_zones' as a comma-separated list. @@ -55,10 +73,30 @@ route53 = False # 'all_instances' to True to return all instances regardless of state. all_instances = False +# By default, only EC2 instances in the 'running' state are returned. Specify +# EC2 instance states to return as a comma-separated list. This +# option is overriden when 'all_instances' is True. +# instance_states = pending, running, shutting-down, terminated, stopping, stopped + # By default, only RDS instances in the 'available' state are returned. Set # 'all_rds_instances' to True return all RDS instances regardless of state. all_rds_instances = False +# Include RDS cluster information (Aurora etc.) +include_rds_clusters = False + +# By default, only ElastiCache clusters and nodes in the 'available' state +# are returned. Set 'all_elasticache_clusters' and/or 'all_elastic_nodes' +# to True return all ElastiCache clusters and nodes, regardless of state. +# +# Note that all_elasticache_nodes only applies to listed clusters. That means +# if you set all_elastic_clusters to false, no node will be return from +# unavailable clusters, regardless of the state and to what you set for +# all_elasticache_nodes. +all_elasticache_replication_groups = False +all_elasticache_clusters = False +all_elasticache_nodes = False + # API calls to EC2 are slow. For this reason, we cache the results of an API # call. Set this to the path you want cache files to be written to. Two files # will be written to this directory: @@ -69,11 +107,18 @@ cache_path = ~/.ansible/tmp # The number of seconds a cache file is considered valid. After this many # seconds, a new API call will be made, and the cache file will be updated. # To disable the cache, set this value to 0 -cache_max_age = 300 +cache_max_age = 0 # Organize groups into a nested/hierarchy instead of a flat namespace. nested_groups = False +# Replace - tags when creating groups to avoid issues with ansible +replace_dash_in_groups = True + +# If set to true, any tag of the form "a,b,c" is expanded into a list +# and the results are used to create additional tag_* inventory groups. +expand_csv_tags = True + # The EC2 inventory output can become very large. To manage its size, # configure which groups should be created. group_by_instance_id = True @@ -89,6 +134,10 @@ group_by_tag_none = True group_by_route53_names = True group_by_rds_engine = True group_by_rds_parameter_group = True +group_by_elasticache_engine = True +group_by_elasticache_cluster = True +group_by_elasticache_parameter_group = True +group_by_elasticache_replication_group = True # If you only want to include hosts that match a certain regular expression # pattern_include = staging-* @@ -113,5 +162,28 @@ group_by_rds_parameter_group = True # You can use wildcards in filter values also. Below will list instances which # tag Name value matches webservers1* -# (ex. webservers15, webservers1a, webservers123 etc) +# (ex. webservers15, webservers1a, webservers123 etc) # instance_filters = tag:Name=webservers1* + +# A boto configuration profile may be used to separate out credentials +# see http://boto.readthedocs.org/en/latest/boto_config_tut.html +# boto_profile = some-boto-profile-name + + +[credentials] + +# The AWS credentials can optionally be specified here. Credentials specified +# here are ignored if the environment variable AWS_ACCESS_KEY_ID or +# AWS_PROFILE is set, or if the boto_profile property above is set. +# +# Supplying AWS credentials here is not recommended, as it introduces +# non-trivial security concerns. When going down this route, please make sure +# to set access permissions for this file correctly, e.g. handle it the same +# way as you would a private SSH key. +# +# Unlike the boto and AWS configure files, this section does not support +# profiles. +# +# aws_access_key_id = AXXXXXXXXXXXXXX +# aws_secret_access_key = XXXXXXXXXXXXXXXXXXX +# aws_security_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXX diff --git a/awx/plugins/inventory/ec2.py b/awx/plugins/inventory/ec2.py index 6068df901f..dcc369e124 100755 --- a/awx/plugins/inventory/ec2.py +++ b/awx/plugins/inventory/ec2.py @@ -37,6 +37,7 @@ When run against a specific host, this script returns the following variables: - ec2_attachTime - ec2_attachment - ec2_attachmentId + - ec2_block_devices - ec2_client_token - ec2_deleteOnTermination - ec2_description @@ -131,6 +132,15 @@ from boto import elasticache from boto import route53 import six +from ansible.module_utils import ec2 as ec2_utils + +HAS_BOTO3 = False +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + pass + from six.moves import configparser from collections import defaultdict @@ -265,6 +275,12 @@ class Ec2Inventory(object): if config.has_option('ec2', 'rds'): self.rds_enabled = config.getboolean('ec2', 'rds') + # Include RDS cluster instances? + if config.has_option('ec2', 'include_rds_clusters'): + self.include_rds_clusters = config.getboolean('ec2', 'include_rds_clusters') + else: + self.include_rds_clusters = False + # Include ElastiCache instances? self.elasticache_enabled = True if config.has_option('ec2', 'elasticache'): @@ -474,6 +490,8 @@ class Ec2Inventory(object): if self.elasticache_enabled: self.get_elasticache_clusters_by_region(region) self.get_elasticache_replication_groups_by_region(region) + if self.include_rds_clusters: + self.include_rds_clusters_by_region(region) self.write_to_cache(self.inventory, self.cache_path_cache) self.write_to_cache(self.index, self.cache_path_index) @@ -527,6 +545,7 @@ class Ec2Inventory(object): instance_ids = [] for reservation in reservations: instance_ids.extend([instance.id for instance in reservation.instances]) + max_filter_value = 199 tags = [] for i in range(0, len(instance_ids), max_filter_value): @@ -573,6 +592,65 @@ class Ec2Inventory(object): error = "Looks like AWS RDS is down:\n%s" % e.message self.fail_with_error(error, 'getting RDS instances') + def include_rds_clusters_by_region(self, region): + if not HAS_BOTO3: + self.fail_with_error("Working with RDS clusters requires boto3 - please install boto3 and try again", + "getting RDS clusters") + + client = ec2_utils.boto3_inventory_conn('client', 'rds', region, **self.credentials) + + marker, clusters = '', [] + while marker is not None: + resp = client.describe_db_clusters(Marker=marker) + clusters.extend(resp["DBClusters"]) + marker = resp.get('Marker', None) + + account_id = boto.connect_iam().get_user().arn.split(':')[4] + c_dict = {} + for c in clusters: + # remove these datetime objects as there is no serialisation to json + # currently in place and we don't need the data yet + if 'EarliestRestorableTime' in c: + del c['EarliestRestorableTime'] + if 'LatestRestorableTime' in c: + del c['LatestRestorableTime'] + + if self.ec2_instance_filters == {}: + matches_filter = True + else: + matches_filter = False + + try: + # arn:aws:rds:::: + tags = client.list_tags_for_resource( + ResourceName='arn:aws:rds:' + region + ':' + account_id + ':cluster:' + c['DBClusterIdentifier']) + c['Tags'] = tags['TagList'] + + if self.ec2_instance_filters: + for filter_key, filter_values in self.ec2_instance_filters.items(): + # get AWS tag key e.g. tag:env will be 'env' + tag_name = filter_key.split(":", 1)[1] + # Filter values is a list (if you put multiple values for the same tag name) + matches_filter = any(d['Key'] == tag_name and d['Value'] in filter_values for d in c['Tags']) + + if matches_filter: + # it matches a filter, so stop looking for further matches + break + + except Exception as e: + if e.message.find('DBInstanceNotFound') >= 0: + # AWS RDS bug (2016-01-06) means deletion does not fully complete and leave an 'empty' cluster. + # Ignore errors when trying to find tags for these + pass + + # ignore empty clusters caused by AWS bug + if len(c['DBClusterMembers']) == 0: + continue + elif matches_filter: + c_dict[c['DBClusterIdentifier']] = c + + self.inventory['db_clusters'] = c_dict + def get_elasticache_clusters_by_region(self, region): ''' Makes an AWS API call to the list of ElastiCache clusters (with nodes' info) in a particular region.''' @@ -1235,7 +1313,7 @@ class Ec2Inventory(object): elif key == 'ec2_tags': for k, v in value.items(): if self.expand_csv_tags and ',' in v: - v = map(lambda x: x.strip(), v.split(',')) + v = list(map(lambda x: x.strip(), v.split(','))) key = self.to_safe('ec2_tag_' + k) instance_vars[key] = v elif key == 'ec2_groups': @@ -1246,6 +1324,10 @@ class Ec2Inventory(object): group_names.append(group.name) instance_vars["ec2_security_group_ids"] = ','.join([str(i) for i in group_ids]) instance_vars["ec2_security_group_names"] = ','.join([str(i) for i in group_names]) + elif key == 'ec2_block_device_mapping': + instance_vars["ec2_block_devices"] = {} + for k, v in value.items(): + instance_vars["ec2_block_devices"][ os.path.basename(k) ] = v.volume_id else: pass # TODO Product codes if someone finds them useful diff --git a/awx/plugins/inventory/gce.py b/awx/plugins/inventory/gce.py index 498511d635..87f1e8e811 100755 --- a/awx/plugins/inventory/gce.py +++ b/awx/plugins/inventory/gce.py @@ -69,7 +69,8 @@ Examples: $ contrib/inventory/gce.py --host my_instance Author: Eric Johnson -Version: 0.0.1 +Contributors: Matt Hite , Tom Melendez +Version: 0.0.3 ''' __requires__ = ['pycrypto>=2.6'] @@ -83,13 +84,19 @@ except ImportError: pass USER_AGENT_PRODUCT="Ansible-gce_inventory_plugin" -USER_AGENT_VERSION="v1" +USER_AGENT_VERSION="v2" import sys import os import argparse + +from time import time + import ConfigParser +import logging +logging.getLogger('libcloud.common.google').addHandler(logging.NullHandler()) + try: import json except ImportError: @@ -100,33 +107,103 @@ try: from libcloud.compute.providers import get_driver _ = Provider.GCE except: - print("GCE inventory script requires libcloud >= 0.13") - sys.exit(1) + sys.exit("GCE inventory script requires libcloud >= 0.13") + + +class CloudInventoryCache(object): + def __init__(self, cache_name='ansible-cloud-cache', cache_path='/tmp', + cache_max_age=300): + cache_dir = os.path.expanduser(cache_path) + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + self.cache_path_cache = os.path.join(cache_dir, cache_name) + + self.cache_max_age = cache_max_age + + def is_valid(self, max_age=None): + ''' Determines if the cache files have expired, or if it is still valid ''' + + if max_age is None: + max_age = self.cache_max_age + + if os.path.isfile(self.cache_path_cache): + mod_time = os.path.getmtime(self.cache_path_cache) + current_time = time() + if (mod_time + max_age) > current_time: + return True + + return False + + def get_all_data_from_cache(self, filename=''): + ''' Reads the JSON inventory from the cache file. Returns Python dictionary. ''' + + data = '' + if not filename: + filename = self.cache_path_cache + with open(filename, 'r') as cache: + data = cache.read() + return json.loads(data) + + def write_to_cache(self, data, filename=''): + ''' Writes data to file as JSON. Returns True. ''' + if not filename: + filename = self.cache_path_cache + json_data = json.dumps(data) + with open(filename, 'w') as cache: + cache.write(json_data) + return True class GceInventory(object): def __init__(self): + # Cache object + self.cache = None + # dictionary containing inventory read from disk + self.inventory = {} + # Read settings and parse CLI arguments self.parse_cli_args() + self.config = self.get_config() self.driver = self.get_gce_driver() + self.ip_type = self.get_inventory_options() + if self.ip_type: + self.ip_type = self.ip_type.lower() + + # Cache management + start_inventory_time = time() + cache_used = False + if self.args.refresh_cache or not self.cache.is_valid(): + self.do_api_calls_update_cache() + else: + self.load_inventory_from_cache() + cache_used = True + self.inventory['_meta']['stats'] = {'use_cache': True} + self.inventory['_meta']['stats'] = { + 'inventory_load_time': time() - start_inventory_time, + 'cache_used': cache_used + } # Just display data for specific host if self.args.host: - print(self.json_format_dict(self.node_to_dict( - self.get_instance(self.args.host)), - pretty=self.args.pretty)) - sys.exit(0) - - zones = self.parse_env_zones() - - # Otherwise, assume user wants all instances grouped - print(self.json_format_dict(self.group_instances(zones), - pretty=self.args.pretty)) + print(self.json_format_dict( + self.inventory['_meta']['hostvars'][self.args.host], + pretty=self.args.pretty)) + else: + # Otherwise, assume user wants all instances grouped + zones = self.parse_env_zones() + print(self.json_format_dict(self.inventory, + pretty=self.args.pretty)) sys.exit(0) - def get_gce_driver(self): - """Determine the GCE authorization settings and return a - libcloud driver. + def get_config(self): + """ + Reads the settings from the gce.ini file. + + Populates a SafeConfigParser object with defaults and + attempts to read an .ini-style configuration from the filename + specified in GCE_INI_PATH. If the environment variable is + not present, the filename defaults to gce.ini in the current + working directory. """ gce_ini_default_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), "gce.ini") @@ -141,14 +218,57 @@ class GceInventory(object): 'gce_service_account_pem_file_path': '', 'gce_project_id': '', 'libcloud_secrets': '', + 'inventory_ip_type': '', + 'cache_path': '~/.ansible/tmp', + 'cache_max_age': '300' }) if 'gce' not in config.sections(): config.add_section('gce') + if 'inventory' not in config.sections(): + config.add_section('inventory') + if 'cache' not in config.sections(): + config.add_section('cache') + config.read(gce_ini_path) + ######### + # Section added for processing ini settings + ######### + + # Set the instance_states filter based on config file options + self.instance_states = [] + if config.has_option('gce', 'instance_states'): + states = config.get('gce', 'instance_states') + # Ignore if instance_states is an empty string. + if states: + self.instance_states = states.split(',') + + # Caching + cache_path = config.get('cache', 'cache_path') + cache_max_age = config.getint('cache', 'cache_max_age') + # TOOD(supertom): support project-specific caches + cache_name = 'ansible-gce.cache' + self.cache = CloudInventoryCache(cache_path=cache_path, + cache_max_age=cache_max_age, + cache_name=cache_name) + return config + + def get_inventory_options(self): + """Determine inventory options. Environment variables always + take precedence over configuration files.""" + ip_type = self.config.get('inventory', 'inventory_ip_type') + # If the appropriate environment variables are set, they override + # other configuration + ip_type = os.environ.get('INVENTORY_IP_TYPE', ip_type) + return ip_type + + def get_gce_driver(self): + """Determine the GCE authorization settings and return a + libcloud driver. + """ # Attempt to get GCE params from a configuration file, if one # exists. - secrets_path = config.get('gce', 'libcloud_secrets') + secrets_path = self.config.get('gce', 'libcloud_secrets') secrets_found = False try: import secrets @@ -162,8 +282,7 @@ class GceInventory(object): if not secrets_path.endswith('secrets.py'): err = "Must specify libcloud secrets file as " err += "/absolute/path/to/secrets.py" - print(err) - sys.exit(1) + sys.exit(err) sys.path.append(os.path.dirname(secrets_path)) try: import secrets @@ -174,10 +293,10 @@ class GceInventory(object): pass if not secrets_found: args = [ - config.get('gce','gce_service_account_email_address'), - config.get('gce','gce_service_account_pem_file_path') + self.config.get('gce','gce_service_account_email_address'), + self.config.get('gce','gce_service_account_pem_file_path') ] - kwargs = {'project': config.get('gce', 'gce_project_id')} + kwargs = {'project': self.config.get('gce', 'gce_project_id')} # If the appropriate environment variables are set, they override # other configuration; process those into our args and kwargs. @@ -211,6 +330,9 @@ class GceInventory(object): help='Get all information about an instance') parser.add_argument('--pretty', action='store_true', default=False, help='Pretty format (default: False)') + parser.add_argument( + '--refresh-cache', action='store_true', default=False, + help='Force refresh of cache by making API requests (default: False - use cache files)') self.args = parser.parse_args() @@ -220,11 +342,17 @@ class GceInventory(object): if inst is None: return {} - if inst.extra['metadata'].has_key('items'): + if 'items' in inst.extra['metadata']: for entry in inst.extra['metadata']['items']: md[entry['key']] = entry['value'] net = inst.extra['networkInterfaces'][0]['network'].split('/')[-1] + # default to exernal IP unless user has specified they prefer internal + if self.ip_type == 'internal': + ssh_host = inst.private_ips[0] + else: + ssh_host = inst.public_ips[0] if len(inst.public_ips) >= 1 else inst.private_ips[0] + return { 'gce_uuid': inst.uuid, 'gce_id': inst.id, @@ -240,15 +368,36 @@ class GceInventory(object): 'gce_metadata': md, 'gce_network': net, # Hosts don't have a public name, so we add an IP - 'ansible_ssh_host': inst.public_ips[0] if len(inst.public_ips) >= 1 else inst.private_ips[0] + 'ansible_ssh_host': ssh_host } - def get_instance(self, instance_name): - '''Gets details about a specific instance ''' + def load_inventory_from_cache(self): + ''' Loads inventory from JSON on disk. ''' + try: - return self.driver.ex_get_node(instance_name) + self.inventory = self.cache.get_all_data_from_cache() + hosts = self.inventory['_meta']['hostvars'] except Exception as e: - return None + print( + "Invalid inventory file %s. Please rebuild with -refresh-cache option." + % (self.cache.cache_path_cache)) + raise + + def do_api_calls_update_cache(self): + ''' Do API calls and save data in cache. ''' + zones = self.parse_env_zones() + data = self.group_instances(zones) + self.cache.write_to_cache(data) + self.inventory = data + + def list_nodes(self): + all_nodes = [] + params, more_results = {'maxResults': 500}, True + while more_results: + self.driver.connection.gce_params=params + all_nodes.extend(self.driver.list_nodes()) + more_results = 'pageToken' in params + return all_nodes def group_instances(self, zones=None): '''Group all instances''' @@ -256,7 +405,18 @@ class GceInventory(object): meta = {} meta["hostvars"] = {} - for node in self.driver.list_nodes(): + for node in self.list_nodes(): + + # This check filters on the desired instance states defined in the + # config file with the instance_states config option. + # + # If the instance_states list is _empty_ then _ALL_ states are returned. + # + # If the instance_states list is _populated_ then check the current + # state against the instance_states list + if self.instance_states and not node.extra['status'] in self.instance_states: + continue + name = node.name meta["hostvars"][name] = self.node_to_dict(node) @@ -268,7 +428,7 @@ class GceInventory(object): if zones and zone not in zones: continue - if groups.has_key(zone): groups[zone].append(name) + if zone in groups: groups[zone].append(name) else: groups[zone] = [name] tags = node.extra['tags'] @@ -277,25 +437,25 @@ class GceInventory(object): tag = t[6:] else: tag = 'tag_%s' % t - if groups.has_key(tag): groups[tag].append(name) + if tag in groups: groups[tag].append(name) else: groups[tag] = [name] net = node.extra['networkInterfaces'][0]['network'].split('/')[-1] net = 'network_%s' % net - if groups.has_key(net): groups[net].append(name) + if net in groups: groups[net].append(name) else: groups[net] = [name] machine_type = node.size - if groups.has_key(machine_type): groups[machine_type].append(name) + if machine_type in groups: groups[machine_type].append(name) else: groups[machine_type] = [name] image = node.image and node.image or 'persistent_disk' - if groups.has_key(image): groups[image].append(name) + if image in groups: groups[image].append(name) else: groups[image] = [name] status = node.extra['status'] stat = 'status_%s' % status.lower() - if groups.has_key(stat): groups[stat].append(name) + if stat in groups: groups[stat].append(name) else: groups[stat] = [name] groups["_meta"] = meta @@ -311,6 +471,6 @@ class GceInventory(object): else: return json.dumps(data) - # Run the script -GceInventory() +if __name__ == '__main__': + GceInventory() diff --git a/awx/plugins/inventory/openstack.py b/awx/plugins/inventory/openstack.py index 103be1bee0..6679a2cc3b 100755 --- a/awx/plugins/inventory/openstack.py +++ b/awx/plugins/inventory/openstack.py @@ -2,7 +2,8 @@ # Copyright (c) 2012, Marco Vito Moscaritolo # Copyright (c) 2013, Jesse Keating -# Copyright (c) 2014, Hewlett-Packard Development Company, L.P. +# Copyright (c) 2015, Hewlett-Packard Development Company, L.P. +# Copyright (c) 2016, Rackspace Australia # # This module is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,7 +19,7 @@ # along with this software. If not, see . # The OpenStack Inventory module uses os-client-config for configuration. -# https://github.com/stackforge/os-client-config +# https://github.com/openstack/os-client-config # This means it will either: # - Respect normal OS_* environment variables like other OpenStack tools # - Read values from a clouds.yaml file. @@ -32,12 +33,24 @@ # all of them and present them as one contiguous inventory. # # See the adjacent openstack.yml file for an example config file +# There are two ansible inventory specific options that can be set in +# the inventory section. +# expand_hostvars controls whether or not the inventory will make extra API +# calls to fill out additional information about each server +# use_hostnames changes the behavior from registering every host with its UUID +# and making a group of its hostname to only doing this if the +# hostname in question has more than one server +# fail_on_errors causes the inventory to fail and return no hosts if one cloud +# has failed (for example, bad credentials or being offline). +# When set to False, the inventory will return hosts from +# whichever other clouds it can contact. (Default: True) import argparse import collections import os import sys import time +from distutils.version import StrictVersion try: import json @@ -46,89 +59,137 @@ except: import os_client_config import shade +import shade.inventory + +CONFIG_FILES = ['/etc/ansible/openstack.yaml', '/etc/ansible/openstack.yml'] -class OpenStackInventory(object): +def get_groups_from_server(server_vars, namegroup=True): + groups = [] - def __init__(self, private=False, refresh=False): - config_files = os_client_config.config.CONFIG_FILES - config_files.append('/etc/ansible/openstack.yml') - self.openstack_config = os_client_config.config.OpenStackConfig( - config_files) - self.clouds = shade.openstack_clouds(self.openstack_config) - self.private = private - self.refresh = refresh + region = server_vars['region'] + cloud = server_vars['cloud'] + metadata = server_vars.get('metadata', {}) - self.cache_max_age = self.openstack_config.get_cache_max_age() - cache_path = self.openstack_config.get_cache_path() + # Create a group for the cloud + groups.append(cloud) - # Cache related - if not os.path.exists(cache_path): - os.makedirs(cache_path) - self.cache_file = os.path.join(cache_path, "ansible-inventory.cache") + # Create a group on region + groups.append(region) - def is_cache_stale(self): - ''' Determines if cache file has expired, or if it is still valid ''' - if os.path.isfile(self.cache_file): - mod_time = os.path.getmtime(self.cache_file) - current_time = time.time() - if (mod_time + self.cache_max_age) > current_time: - return False - return True + # And one by cloud_region + groups.append("%s_%s" % (cloud, region)) - def get_host_groups(self): - if self.refresh or self.is_cache_stale(): - groups = self.get_host_groups_from_cloud() - self.write_cache(groups) + # Check if group metadata key in servers' metadata + if 'group' in metadata: + groups.append(metadata['group']) + + for extra_group in metadata.get('groups', '').split(','): + if extra_group: + groups.append(extra_group.strip()) + + groups.append('instance-%s' % server_vars['id']) + if namegroup: + groups.append(server_vars['name']) + + for key in ('flavor', 'image'): + if 'name' in server_vars[key]: + groups.append('%s-%s' % (key, server_vars[key]['name'])) + + for key, value in iter(metadata.items()): + groups.append('meta-%s_%s' % (key, value)) + + az = server_vars.get('az', None) + if az: + # Make groups for az, region_az and cloud_region_az + groups.append(az) + groups.append('%s_%s' % (region, az)) + groups.append('%s_%s_%s' % (cloud, region, az)) + return groups + + +def get_host_groups(inventory, refresh=False): + (cache_file, cache_expiration_time) = get_cache_settings() + if is_cache_stale(cache_file, cache_expiration_time, refresh=refresh): + groups = to_json(get_host_groups_from_cloud(inventory)) + open(cache_file, 'w').write(groups) + else: + groups = open(cache_file, 'r').read() + return groups + + +def append_hostvars(hostvars, groups, key, server, namegroup=False): + hostvars[key] = dict( + ansible_ssh_host=server['interface_ip'], + openstack=server) + for group in get_groups_from_server(server, namegroup=namegroup): + groups[group].append(key) + + +def get_host_groups_from_cloud(inventory): + groups = collections.defaultdict(list) + firstpass = collections.defaultdict(list) + hostvars = {} + list_args = {} + if hasattr(inventory, 'extra_config'): + use_hostnames = inventory.extra_config['use_hostnames'] + list_args['expand'] = inventory.extra_config['expand_hostvars'] + if StrictVersion(shade.__version__) >= StrictVersion("1.6.0"): + list_args['fail_on_cloud_config'] = \ + inventory.extra_config['fail_on_errors'] + else: + use_hostnames = False + + for server in inventory.list_hosts(**list_args): + + if 'interface_ip' not in server: + continue + firstpass[server['name']].append(server) + for name, servers in firstpass.items(): + if len(servers) == 1 and use_hostnames: + append_hostvars(hostvars, groups, name, servers[0]) else: - return json.load(open(self.cache_file, 'r')) - return groups + server_ids = set() + # Trap for duplicate results + for server in servers: + server_ids.add(server['id']) + if len(server_ids) == 1 and use_hostnames: + append_hostvars(hostvars, groups, name, servers[0]) + else: + for server in servers: + append_hostvars( + hostvars, groups, server['id'], server, + namegroup=True) + groups['_meta'] = {'hostvars': hostvars} + return groups - def write_cache(self, groups): - with open(self.cache_file, 'w') as cache_file: - cache_file.write(self.json_format_dict(groups)) - def get_host_groups_from_cloud(self): - groups = collections.defaultdict(list) - hostvars = collections.defaultdict(dict) +def is_cache_stale(cache_file, cache_expiration_time, refresh=False): + ''' Determines if cache file has expired, or if it is still valid ''' + if refresh: + return True + if os.path.isfile(cache_file) and os.path.getsize(cache_file) > 0: + mod_time = os.path.getmtime(cache_file) + current_time = time.time() + if (mod_time + cache_expiration_time) > current_time: + return False + return True - for cloud in self.clouds: - cloud.private = cloud.private or self.private - # Cycle on servers - for server in cloud.list_servers(): +def get_cache_settings(): + config = os_client_config.config.OpenStackConfig( + config_files=os_client_config.config.CONFIG_FILES + CONFIG_FILES) + # For inventory-wide caching + cache_expiration_time = config.get_cache_expiration_time() + cache_path = config.get_cache_path() + if not os.path.exists(cache_path): + os.makedirs(cache_path) + cache_file = os.path.join(cache_path, 'ansible-inventory.cache') + return (cache_file, cache_expiration_time) - meta = cloud.get_server_meta(server) - if 'interface_ip' not in meta['server_vars']: - # skip this host if it doesn't have a network address - continue - - server_vars = meta['server_vars'] - hostvars[server.name][ - 'ansible_ssh_host'] = server_vars['interface_ip'] - hostvars[server.name]['openstack'] = server_vars - - for group in meta['groups']: - groups[group].append(server.name) - - if hostvars: - groups['_meta'] = {'hostvars': hostvars} - return groups - - def json_format_dict(self, data): - return json.dumps(data, sort_keys=True, indent=2) - - def list_instances(self): - groups = self.get_host_groups() - # Return server list - print(self.json_format_dict(groups)) - - def get_host(self, hostname): - groups = self.get_host_groups() - hostvars = groups['_meta']['hostvars'] - if hostname in hostvars: - print(self.json_format_dict(hostvars[hostname])) +def to_json(in_dict): + return json.dumps(in_dict, sort_keys=True, indent=2) def parse_args(): @@ -138,21 +199,43 @@ def parse_args(): help='Use private address for ansible host') parser.add_argument('--refresh', action='store_true', help='Refresh cached information') + parser.add_argument('--debug', action='store_true', default=False, + help='Enable debug output') group = parser.add_mutually_exclusive_group(required=True) group.add_argument('--list', action='store_true', help='List active servers') group.add_argument('--host', help='List details about the specific host') + return parser.parse_args() def main(): args = parse_args() try: - inventory = OpenStackInventory(args.private, args.refresh) + config_files = os_client_config.config.CONFIG_FILES + CONFIG_FILES + shade.simple_logging(debug=args.debug) + inventory_args = dict( + refresh=args.refresh, + config_files=config_files, + private=args.private, + ) + if hasattr(shade.inventory.OpenStackInventory, 'extra_config'): + inventory_args.update(dict( + config_key='ansible', + config_defaults={ + 'use_hostnames': False, + 'expand_hostvars': True, + 'fail_on_errors': True, + } + )) + + inventory = shade.inventory.OpenStackInventory(**inventory_args) + if args.list: - inventory.list_instances() + output = get_host_groups(inventory, refresh=args.refresh) elif args.host: - inventory.get_host(args.host) + output = to_json(inventory.get_host(args.host)) + print(output) except shade.OpenStackCloudException as e: sys.stderr.write('%s\n' % e.message) sys.exit(1) diff --git a/awx/plugins/inventory/openstack.yml b/awx/plugins/inventory/openstack.yml index a99bb02058..3687b1f399 100644 --- a/awx/plugins/inventory/openstack.yml +++ b/awx/plugins/inventory/openstack.yml @@ -26,3 +26,7 @@ clouds: username: stack password: stack project_name: stack +ansible: + use_hostnames: False + expand_hostvars: True + fail_on_errors: True diff --git a/awx/plugins/inventory/vmware.py b/awx/plugins/inventory/vmware.py index 8f723a638d..377c7cb83a 100755 --- a/awx/plugins/inventory/vmware.py +++ b/awx/plugins/inventory/vmware.py @@ -35,11 +35,12 @@ import json import logging import optparse import os +import ssl import sys import time import ConfigParser -from six import text_type +from six import text_type, string_types # Disable logging message trigged by pSphere/suds. try: @@ -54,7 +55,7 @@ logging.getLogger('suds').addHandler(NullHandler()) from psphere.client import Client from psphere.errors import ObjectNotFoundError -from psphere.managedobjects import HostSystem, VirtualMachine, ManagedObject, Network +from psphere.managedobjects import HostSystem, VirtualMachine, ManagedObject, Network, ClusterComputeResource from suds.sudsobject import Object as SudsObject @@ -90,6 +91,28 @@ class VMwareInventory(object): auth_password = os.environ.get('VMWARE_PASSWORD') if not auth_password and self.config.has_option('auth', 'password'): auth_password = self.config.get('auth', 'password') + sslcheck = os.environ.get('VMWARE_SSLCHECK') + if not sslcheck and self.config.has_option('auth', 'sslcheck'): + sslcheck = self.config.get('auth', 'sslcheck') + if not sslcheck: + sslcheck = True + else: + if sslcheck.lower() in ['no', 'false']: + sslcheck = False + else: + sslcheck = True + + # Limit the clusters being scanned + self.filter_clusters = os.environ.get('VMWARE_CLUSTERS') + if not self.filter_clusters and self.config.has_option('defaults', 'clusters'): + self.filter_clusters = self.config.get('defaults', 'clusters') + if self.filter_clusters: + self.filter_clusters = [x.strip() for x in self.filter_clusters.split(',') if x.strip()] + + # Override certificate checks + if not sslcheck: + if hasattr(ssl, '_create_unverified_context'): + ssl._create_default_https_context = ssl._create_unverified_context # Create the VMware client connection. self.client = Client(auth_host, auth_user, auth_password) @@ -137,7 +160,7 @@ class VMwareInventory(object): if isinstance(v, collections.MutableMapping): items.extend(self._flatten_dict(v, new_key, sep).items()) elif isinstance(v, (list, tuple)): - if all([isinstance(x, basestring) for x in v]): + if all([isinstance(x, string_types) for x in v]): items.append((new_key, v)) else: items.append((new_key, v)) @@ -185,7 +208,7 @@ class VMwareInventory(object): if obj_info != (): l.append(obj_info) return l - elif isinstance(obj, (type(None), bool, int, long, float, basestring)): + elif isinstance(obj, (type(None), bool, int, long, float, string_types)): return obj else: return () @@ -314,8 +337,19 @@ class VMwareInventory(object): else: prefix_filter = None + if self.filter_clusters: + # Loop through clusters and find hosts: + hosts = [] + for cluster in ClusterComputeResource.all(self.client): + if cluster.name in self.filter_clusters: + for host in cluster.host: + hosts.append(host) + else: + # Get list of all physical hosts + hosts = HostSystem.all(self.client) + # Loop through physical hosts: - for host in HostSystem.all(self.client): + for host in hosts: if not self.guests_only: self._add_host(inv, 'all', host.name) From 4532945d5ac2ec4773cfe4ff3f3812cda33319f0 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 13 Dec 2016 14:35:51 -0800 Subject: [PATCH 124/595] adding result_traceback to the UI --- .../src/job-results/job-results.partial.html | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index cf45b792aa..340d4b1ca5 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -85,6 +85,19 @@
+ +
+ +
+
+
+ +
@@ -401,23 +414,6 @@ - -
From 49259e3588a47f226df864a4fcd4291304be4b20 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 13 Dec 2016 16:26:29 -0800 Subject: [PATCH 125/595] adding explanation to left hand panel --- .../src/job-results/job-results.controller.js | 4 +++- .../src/job-results/job-results.partial.html | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index ce92fb95f7..adfe541bc4 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -85,7 +85,9 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' jobData.summary_fields.source_workflow_job.id){ $scope.workflow_result_link = `/#/workflows/${jobData.summary_fields.source_workflow_job.id}`; } - + if(jobData.result_traceback) { + $scope.job.result_traceback = jobData.result_traceback.trim().split('\n').join('
'); + } // use options labels to manipulate display of details getTowerLabels(); diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 340d4b1ca5..4b2d8be6ea 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -85,14 +85,24 @@
+ +
+ +
+ {{job.job_explanation}} +
+
+
-
@@ -410,7 +420,7 @@
-
--> + From ea10ff8b936ac5bdaf852d48522b996f356c2b0d Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 13 Dec 2016 21:44:09 -0500 Subject: [PATCH 126/595] Initial pass at related search fields. --- awx/api/filters.py | 29 +++++++++++++++++++++++++-- awx/api/generics.py | 17 +++++++++++++++- awx/api/metadata.py | 4 ++++ awx/api/templates/api/_list_common.md | 4 ++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/awx/api/filters.py b/awx/api/filters.py index 5146ff0cd2..9128af7e81 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -77,7 +77,7 @@ class FieldLookupBackend(BaseFilterBackend): SUPPORTED_LOOKUPS = ('exact', 'iexact', 'contains', 'icontains', 'startswith', 'istartswith', 'endswith', 'iendswith', 'regex', 'iregex', 'gt', 'gte', 'lt', 'lte', 'in', - 'isnull') + 'isnull', 'search') def get_field_from_lookup(self, model, lookup): field = None @@ -148,6 +148,15 @@ class FieldLookupBackend(BaseFilterBackend): re.compile(value) except re.error as e: raise ValueError(e.args[0]) + elif new_lookup.endswith('__search'): + related_model = getattr(field, 'related_model', None) + if not related_model: + raise ValueError('%s is not searchable' % new_lookup[:-8]) + new_lookups = [] + for rm_field in related_model._meta.fields: + if rm_field.name in ('username', 'first_name', 'last_name', 'email', 'name', 'description'): + new_lookups.append('{}__{}__icontains'.format(new_lookup[:-8], rm_field.name)) + return value, new_lookups else: value = self.value_to_python_for_field(field, value) return value, new_lookup @@ -160,6 +169,7 @@ class FieldLookupBackend(BaseFilterBackend): or_filters = [] chain_filters = [] role_filters = [] + search_filters = [] for key, values in request.query_params.lists(): if key in self.RESERVED_NAMES: continue @@ -181,6 +191,16 @@ class FieldLookupBackend(BaseFilterBackend): role_filters.append(values[0]) continue + # Search across related objects. + if key.endswith('__search'): + for value in values: + for search_term in force_text(value).replace(',', ' ').split(): + search_value, new_keys = self.value_to_python(queryset.model, key, search_term) + assert isinstance(new_keys, list) + for new_key in new_keys: + search_filters.append((new_key, search_value)) + continue + # Custom chain__ and or__ filters, mutually exclusive (both can # precede not__). q_chain = False @@ -211,7 +231,7 @@ class FieldLookupBackend(BaseFilterBackend): and_filters.append((q_not, new_key, value)) # Now build Q objects for database query filter. - if and_filters or or_filters or chain_filters or role_filters: + if and_filters or or_filters or chain_filters or role_filters or search_filters: args = [] for n, k, v in and_filters: if n: @@ -234,6 +254,11 @@ class FieldLookupBackend(BaseFilterBackend): else: q |= Q(**{k:v}) args.append(q) + if search_filters: + q = Q() + for k,v in search_filters: + q |= Q(**{k:v}) + args.append(q) for n,k,v in chain_filters: if n: q = ~Q(**{k:v}) diff --git a/awx/api/generics.py b/awx/api/generics.py index 1062135a28..73b92cfcc5 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -267,10 +267,25 @@ class ListAPIView(generics.ListAPIView, GenericAPIView): fields = [] for field in self.model._meta.fields: if field.name in ('username', 'first_name', 'last_name', 'email', - 'name', 'description', 'email'): + 'name', 'description'): fields.append(field.name) return fields + @property + def related_search_fields(self): + fields = [] + for field in self.model._meta.fields: + if field.name.endswith('_role'): + continue + if getattr(field, 'related_model', None): + fields.append('{}__search'.format(field.name)) + for rel in self.model._meta.related_objects: + name = rel.get_accessor_name() + if name.endswith('_set'): + continue + fields.append('{}__search'.format(name)) + return fields + class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView): # Base class for a list view that allows creating new objects. diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 6dd186c9ef..fb5c4d6493 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -182,6 +182,10 @@ class Metadata(metadata.SimpleMetadata): if getattr(view, 'search_fields', None): metadata['search_fields'] = view.search_fields + # Add related search fields if available from the view. + if getattr(view, 'related_search_fields', None): + metadata['related_search_fields'] = view.related_search_fields + return metadata diff --git a/awx/api/templates/api/_list_common.md b/awx/api/templates/api/_list_common.md index 36e6819276..706ae732a5 100644 --- a/awx/api/templates/api/_list_common.md +++ b/awx/api/templates/api/_list_common.md @@ -56,6 +56,10 @@ within all designated text fields of a model. _Added in AWX 1.4_ +(_Added in Ansible Tower 3.1.0_) Search across related fields: + + ?related__search=findme + ## Filtering Any additional query string parameters may be used to filter the list of From 24ee84b0946f0fe21db887c8429b6ff894eb4abf Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 13 Dec 2016 17:54:17 -0500 Subject: [PATCH 127/595] Fixed various bugs after auditing the workflow details page --- .../workflow-chart/workflow-chart.block.less | 8 +++ .../workflow-chart.directive.js | 67 +++++++++++++------ .../templates/workflows/workflow.service.js | 10 ++- .../workflow-results.partial.html | 43 +++++------- 4 files changed, 76 insertions(+), 52 deletions(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less index 7263edbf02..4399e6504a 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less @@ -105,3 +105,11 @@ .WorkflowChart-activeNode { fill: @default-link; } +.WorkflowChart-elapsedHolder { + background-color: @b7grey; + color: @default-bg; + height: 13px; + width: 39px; + padding: 1px 3px; + border-radius: 4px; +} diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index cb6eeafa28..e91cc7c2d8 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -4,8 +4,8 @@ * All Rights Reserved *************************************************/ -export default [ '$state', - function($state) { +export default [ '$state', 'moment', + function($state, moment) { return { scope: { @@ -188,6 +188,7 @@ export default [ '$state', .attr("dy", ".35em") .attr("class", "WorkflowChart-startText") .text(function () { return "START"; }) + .attr("display", function() { return scope.mode === 'details' ? 'none' : null;}) .call(add_node); } else { @@ -223,10 +224,10 @@ export default [ '$state', .style("display", function(d) { return d.isActiveEdit ? null : "none"; }); thisNode.append("text") - .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 20 : rectW / 2; }) - .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 10 : rectH / 2; }) + .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : rectW / 2; }) + .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : rectH / 2; }) .attr("dy", ".35em") - .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? "inherit" : "middle"; }) + .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; }) .attr("class", "WorkflowChart-defaultText WorkflowChart-nameText") .text(function (d) { return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : ""; @@ -291,7 +292,7 @@ export default [ '$state', .attr("y", rectH - 10) .attr("dy", ".35em") .attr("class", "WorkflowChart-detailsLink") - .style("display", function(d){ return d.job && d.job.jobStatus && d.job.unified_job_id ? null : "none"; }) + .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }) .text(function () { return "DETAILS"; }) @@ -386,7 +387,7 @@ export default [ '$state', let statusClass = "WorkflowChart-nodeStatus "; if(d.job){ - switch(d.job.jobStatus) { + switch(d.job.status) { case "pending": statusClass = "workflowChart-nodeStatus--running"; break; @@ -402,15 +403,37 @@ export default [ '$state', case "failed": statusClass = "workflowChart-nodeStatus--failed"; break; + case "error": + statusClass = "workflowChart-nodeStatus--failed"; + break; } } return statusClass; }) - .style("display", function(d) { return d.job && d.job.jobStatus ? null : "none"; }) + .style("display", function(d) { return d.job && d.job.status ? null : "none"; }) .attr("cy", 10) .attr("cx", 10) .attr("r", 6); + + thisNode.append("foreignObject") + .attr("x", 5) + .attr("y", 43) + .style("font-size","0.7em") + .attr("class", "WorkflowChart-elapsed") + .html(function (d) { + if(d.job && d.job.elapsed) { + let elapsedMs = d.job.elapsed * 1000; + let elapsedMoment = moment.duration(elapsedMs); + let paddedElapsedMoment = Math.floor(elapsedMoment.asHours()) < 10 ? "0" + Math.floor(elapsedMoment.asHours()) : Math.floor(elapsedMoment.asHours()); + let elapsedString = paddedElapsedMoment + moment.utc(elapsedMs).format(":mm:ss"); + return "
" + elapsedString + "
"; + } + else { + return ""; + } + }) + .style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; }); } }); @@ -588,7 +611,7 @@ export default [ '$state', let statusClass = "WorkflowChart-nodeStatus "; if(d.job){ - switch(d.job.jobStatus) { + switch(d.job.status) { case "pending": statusClass += "workflowChart-nodeStatus--running"; break; @@ -604,17 +627,20 @@ export default [ '$state', case "failed": statusClass += "workflowChart-nodeStatus--failed"; break; + case "error": + statusClass = "workflowChart-nodeStatus--failed"; + break; } } return statusClass; }) - .style("display", function(d) { return d.job && d.job.jobStatus ? null : "none"; }) + .style("display", function(d) { return d.job && d.job.status ? null : "none"; }) .transition() .duration(0) .attr("r", 6) .each(function(d) { - if(d.job && d.job.jobStatus && (d.job.jobStatus === "pending" || d.job.jobStatus === "waiting" || d.job.jobStatus === "running")) { + if(d.job && d.job.status && (d.job.status === "pending" || d.job.status === "waiting" || d.job.status === "running")) { // Pulse the circle var circle = d3.select(this); (function repeat() { @@ -631,15 +657,15 @@ export default [ '$state', }); t.selectAll(".WorkflowChart-nameText") - .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 20 : rectW / 2; }) - .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 10 : rectH / 2; }) - .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? "inherit" : "middle"; }) + .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : rectW / 2; }) + .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : rectH / 2; }) + .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; }) .text(function (d) { return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? wrap(d.unifiedJobTemplate.name) : ""; }); t.selectAll(".WorkflowChart-detailsLink") - .style("display", function(d){ return d.job && d.job.jobStatus && d.job.unified_job_id ? null : "none"; }); + .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }); t.selectAll(".WorkflowChart-incompleteText") .style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; }); @@ -650,6 +676,9 @@ export default [ '$state', t.selectAll(".WorkflowChart-activeNode") .style("display", function(d) { return d.isActiveEdit ? null : "none"; }); + t.selectAll(".WorkflowChart-elapsed") + .style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; }); + } function add_node() { @@ -702,15 +731,15 @@ export default [ '$state', d3.select(this).style("text-decoration", null); }); this.on("click", function(d) { - if(d.job.unified_job_id && d.unifiedJobTemplate) { + if(d.job.id && d.unifiedJobTemplate) { if(d.unifiedJobTemplate.unified_job_type === 'job') { - $state.go('jobDetail', {id: d.job.unified_job_id}); + $state.go('jobDetail', {id: d.job.id}); } else if(d.unifiedJobTemplate.unified_job_type === 'inventory_update') { - $state.go('inventorySyncStdout', {id: d.job.unified_job_id}); + $state.go('inventorySyncStdout', {id: d.job.id}); } else if(d.unifiedJobTemplate.unified_job_type === 'project_update') { - $state.go('scmUpdateStdout', {id: d.job.unified_job_id}); + $state.go('scmUpdateStdout', {id: d.job.id}); } } }); diff --git a/awx/ui/client/src/templates/workflows/workflow.service.js b/awx/ui/client/src/templates/workflows/workflow.service.js index 4e551c7582..f355c306f2 100644 --- a/awx/ui/client/src/templates/workflows/workflow.service.js +++ b/awx/ui/client/src/templates/workflows/workflow.service.js @@ -224,10 +224,8 @@ export default [function(){ } if(params.nodesObj[params.nodeId].summary_fields.job) { - treeNode.job = { - jobStatus: params.nodesObj[params.nodeId].summary_fields.job.status, - unified_job_id: params.nodesObj[params.nodeId].summary_fields.job.id - }; + treeNode.job = _.clone(params.nodesObj[params.nodeId].summary_fields.job); + //treeNode.job.unified_job_id = params.nodesObj[params.nodeId].summary_fields.job.id; } if(params.nodesObj[params.nodeId].summary_fields.unified_job_template) { @@ -282,8 +280,8 @@ export default [function(){ if(matchingNode) { matchingNode.job = { - jobStatus: params.status, - unified_job_id: params.unified_job_id + status: params.status, + id: params.unified_job_id }; } diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index ac04b114e8..b35ca65768 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -62,6 +62,21 @@
+ +
+ + +
+
@@ -85,32 +100,6 @@
- -
- - -
- - -
- -
- Workflow Job -
-
-
@@ -265,7 +254,7 @@
- + From 0ff5c06b2b2ae4e4bc7393d952df0dd18bcbcbfc Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 14 Dec 2016 09:41:32 -0500 Subject: [PATCH 128/595] Removed moment injection from the workflow chart directive - not needed --- .../workflows/workflow-chart/workflow-chart.directive.js | 4 ++-- awx/ui/client/src/templates/workflows/workflow.service.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index e91cc7c2d8..e4557284b6 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -4,8 +4,8 @@ * All Rights Reserved *************************************************/ -export default [ '$state', 'moment', - function($state, moment) { +export default [ '$state', + function($state) { return { scope: { diff --git a/awx/ui/client/src/templates/workflows/workflow.service.js b/awx/ui/client/src/templates/workflows/workflow.service.js index f355c306f2..64b939ddcb 100644 --- a/awx/ui/client/src/templates/workflows/workflow.service.js +++ b/awx/ui/client/src/templates/workflows/workflow.service.js @@ -225,7 +225,6 @@ export default [function(){ if(params.nodesObj[params.nodeId].summary_fields.job) { treeNode.job = _.clone(params.nodesObj[params.nodeId].summary_fields.job); - //treeNode.job.unified_job_id = params.nodesObj[params.nodeId].summary_fields.job.id; } if(params.nodesObj[params.nodeId].summary_fields.unified_job_template) { From 6348ed97dd5a983c791ac2c321ec2dea680d90b7 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 14 Dec 2016 09:53:24 -0500 Subject: [PATCH 129/595] uninstall certifi if installed in venv's --- Makefile | 5 +++++ requirements/README | 10 ---------- requirements/README.md | 19 +++++++++++++++++++ .../requirements_ansible_uninstall.txt | 1 + requirements/requirements_dev_uninstall.txt | 1 + requirements/requirements_tower_uninstall.txt | 1 + 6 files changed, 27 insertions(+), 10 deletions(-) delete mode 100644 requirements/README create mode 100644 requirements/README.md create mode 100644 requirements/requirements_ansible_uninstall.txt create mode 100644 requirements/requirements_dev_uninstall.txt create mode 100644 requirements/requirements_tower_uninstall.txt diff --git a/Makefile b/Makefile index b32cf7b35c..684b267244 100644 --- a/Makefile +++ b/Makefile @@ -285,8 +285,10 @@ requirements_ansible: virtualenv_ansible if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/ansible/bin/activate; \ $(VENV_BASE)/ansible/bin/pip install --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements_ansible.txt ;\ + $(VENV_BASE)/ansible/bin/pip uninstall --yes -r requirements/requirements_ansible_uninstall.txt; \ else \ pip install --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements_ansible.txt ; \ + pip uninstall --yes -r requirements/requirements_ansible_uninstall.txt; \ fi # Install third-party requirements needed for Tower's environment. @@ -294,14 +296,17 @@ requirements_tower: virtualenv_tower if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/tower/bin/activate; \ $(VENV_BASE)/tower/bin/pip install --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements.txt ;\ + $(VENV_BASE)/tower/bin/pip uninstall --yes -r requirements/requirements_tower_uninstall.txt; \ else \ pip install --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements.txt ; \ + pip uninstall --yes -r requirements/requirements_tower_uninstall.txt; \ fi requirements_tower_dev: if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/tower/bin/activate; \ $(VENV_BASE)/tower/bin/pip install -r requirements/requirements_dev.txt; \ + $(VENV_BASE)/tower/bin/pip uninstall --yes -r requirements/requirements_dev_uninstall.txt; \ fi # Install third-party requirements needed for running unittests in jenkins diff --git a/requirements/README b/requirements/README deleted file mode 100644 index 26348a68de..0000000000 --- a/requirements/README +++ /dev/null @@ -1,10 +0,0 @@ -To find packages missing from requirements.txt run the below command and look for packages after the example listed below. - -`PYTHONPATH=awx/lib/site-packages/ pip freeze -r requirements/requirements.txt` - -``` -... -## The following requirements were added by pip freeze: -functools32==3.2.3.post2 -... -``` diff --git a/requirements/README.md b/requirements/README.md new file mode 100644 index 0000000000..f5080fadde --- /dev/null +++ b/requirements/README.md @@ -0,0 +1,19 @@ +The requirements.txt and requirements_ansible.txt files are generated from requirements.in and requirements_ansible.in, respectively, using `pip-tools` `pip-compile`. + +``` +virtualenv /buildit +source /buildit/bin/activate +pip install pip-tools +pip install pip --upgrade + +pip-compile requirements/requirements.in > requirements/requirements.txt +pip-compile requirements/requirements_ansible.in > requirements/requirements_ansible.txt +``` + +## Known Issues + +* Remove the `-e` from packages of the form `-e git+https://github.com...` in the generated `.txt`. Failure to do so will result in a "bad" RPM and DEB due to the `pip install` laying down a symbolic link with an absolute path from the virtualenv to the git repository that will differ from when the RPM and DEB are build to when the RPM and DEB are installed on a machine. By removing the `-e` the symbolic egg link will not be created and all is well. + +* As of `pip-tools` `1.8.1` `pip-compile` does not resolve packages specified using a git url. Thus, dependencies for things like `dm.xmlsec.binding` do not get resolved and output to `requirements.txt`. This means that: + * can't use `pip install --no-deps` because other deps WILL be sucked in + * all dependencies are NOT captured in our `.txt` files. This means you can't rely on the `.txt` when gathering licenses. diff --git a/requirements/requirements_ansible_uninstall.txt b/requirements/requirements_ansible_uninstall.txt new file mode 100644 index 0000000000..963eac530b --- /dev/null +++ b/requirements/requirements_ansible_uninstall.txt @@ -0,0 +1 @@ +certifi diff --git a/requirements/requirements_dev_uninstall.txt b/requirements/requirements_dev_uninstall.txt new file mode 100644 index 0000000000..963eac530b --- /dev/null +++ b/requirements/requirements_dev_uninstall.txt @@ -0,0 +1 @@ +certifi diff --git a/requirements/requirements_tower_uninstall.txt b/requirements/requirements_tower_uninstall.txt new file mode 100644 index 0000000000..963eac530b --- /dev/null +++ b/requirements/requirements_tower_uninstall.txt @@ -0,0 +1 @@ +certifi From 94aafda7dfd3a1dbea16111841bdbbcedff444af Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Wed, 14 Dec 2016 10:15:07 -0500 Subject: [PATCH 130/595] Added logging, timeouts, changed dropdown labels, updated system section --- .../configuration-auth.controller.js | 6 +- .../auth-form/configuration-auth.partial.html | 7 +- .../configuration/configuration.block.less | 19 +- .../configuration/configuration.controller.js | 36 +++- .../jobs-form/configuration-jobs.form.js | 14 +- awx/ui/client/src/configuration/main.js | 13 +- .../configuration-system.controller.js | 165 ++++++++++++++++-- .../configuration-system.partial.html | 31 +++- .../sub-forms/system-activity-stream.form.js | 38 ++++ .../sub-forms/system-logging.form.js | 63 +++++++ .../system-form/sub-forms/system-misc.form.js | 42 +++++ 11 files changed, 396 insertions(+), 38 deletions(-) create mode 100644 awx/ui/client/src/configuration/system-form/sub-forms/system-activity-stream.form.js create mode 100644 awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js create mode 100644 awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js index 33d1a53c37..5c3d31a0bc 100644 --- a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js @@ -99,9 +99,9 @@ export default [ var dropdownOptions = [ {label: i18n._('Azure AD'), value: 'azure'}, - {label: i18n._('Github'), value: 'github'}, - {label: i18n._('Github Org'), value: 'github_org'}, - {label: i18n._('Github Team'), value: 'github_team'}, + {label: i18n._('GitHub'), value: 'github'}, + {label: i18n._('GitHub Org'), value: 'github_org'}, + {label: i18n._('GithHub Team'), value: 'github_team'}, {label: i18n._('Google OAuth2'), value: 'google_oauth'}, {label: i18n._('LDAP'), value: 'ldap'}, {label: i18n._('RADIUS'), value: 'radius'}, diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html b/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html index 5efeeed532..71192e17c6 100644 --- a/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html @@ -1,7 +1,7 @@
- -
+
+
Sub Category
+
+
diff --git a/awx/ui/client/src/configuration/configuration.block.less b/awx/ui/client/src/configuration/configuration.block.less index de4dae2e2c..0b2a1ea0ee 100644 --- a/awx/ui/client/src/configuration/configuration.block.less +++ b/awx/ui/client/src/configuration/configuration.block.less @@ -21,11 +21,26 @@ margin-left: 0; } -.Form-nav--dropdown { - width: 175px; +.Form-nav--dropdownContainer { + width: 285px; margin-top: -52px; margin-bottom: 22px; margin-left: auto; + display: flex; + justify-content: space-between; +} + +.Form-nav--dropdown { + width: 60%; +} + +.Form-nav--dropdownLabel { + text-transform: uppercase; + color: @default-interface-txt; + font-size: 14px; + font-weight: bold; + padding-right: 5px; + padding-top: 5px; } .Form-tabRow { diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js index e5d3d6049d..153434ef84 100644 --- a/awx/ui/client/src/configuration/configuration.controller.js +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -17,8 +17,10 @@ export default [ 'configurationLdapForm', 'configurationRadiusForm', 'configurationSamlForm', + 'systemActivityStreamForm', + 'systemLoggingForm', + 'systemMiscForm', 'ConfigurationJobsForm', - 'ConfigurationSystemForm', 'ConfigurationUiForm', function( $scope, $rootScope, $state, $stateParams, $timeout, $q, Alert, ClearScope, @@ -33,8 +35,10 @@ export default [ configurationLdapForm, configurationRadiusForm, configurationSamlForm, + systemActivityStreamForm, + systemLoggingForm, + systemMiscForm, ConfigurationJobsForm, - ConfigurationSystemForm, ConfigurationUiForm ) { var vm = this; @@ -48,8 +52,10 @@ export default [ 'ldap': configurationLdapForm, 'radius': configurationRadiusForm, 'saml': configurationSamlForm, + 'activity_stream': systemActivityStreamForm, + 'logging': systemLoggingForm, + 'misc': systemMiscForm, 'jobs': ConfigurationJobsForm, - 'system': ConfigurationSystemForm, 'ui': ConfigurationUiForm }; @@ -84,19 +90,24 @@ export default [ lastForm: '', currentForm: '', currentAuth: '', + currentSystem: '', setCurrent: function(form) { this.lastForm = this.currentForm; this.currentForm = form; }, - setCurrentAuth: function(form) { - this.currentAuth = form; - this.setCurrent(this.currentAuth); - }, getCurrent: function() { return this.currentForm; }, currentFormName: function() { return 'configuration_' + this.currentForm + '_template_form'; + }, + setCurrentAuth: function(form) { + this.currentAuth = form; + this.setCurrent(this.currentAuth); + }, + setCurrentSystem: function(form) { + this.currentSystem = form; + this.setCurrent(this.currentSystem); } }; @@ -182,6 +193,7 @@ export default [ } function active(setForm) { + // Authentication and System's sub-module dropdowns handled first: if (setForm === 'auth') { // Default to 'azure' on first load if (formTracker.currentAuth === '') { @@ -190,7 +202,15 @@ export default [ // If returning to auth tab reset current form to previously viewed formTracker.setCurrentAuth(formTracker.currentAuth); } - } else { + } else if (setForm === 'system') { + if (formTracker.currentSystem === '') { + formTracker.setCurrentSystem('misc'); + } else { + // If returning to system tab reset current form to previously viewed + formTracker.setCurrentSystem(formTracker.currentSystem); + } + } + else { formTracker.setCurrent(setForm); } vm.activeTab = setForm; diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js index 05b9d664a7..caf0392c24 100644 --- a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js @@ -46,7 +46,19 @@ }, AWX_PROOT_ENABLED: { type: 'toggleSwitch', - } + }, + DEFAULT_JOB_TIMEOUT: { + type: 'text', + reset: 'DEFAULT_JOB_TIMEOUT', + }, + DEFAULT_INVENTORY_UPDATE_TIMEOUT: { + type: 'text', + reset: 'DEFAULT_INVENTORY_UPDATE_TIMEOUT', + }, + DEFAULT_PROJECT_UPDATE_TIMEOUT: { + type: 'text', + reset: 'DEFAULT_PROJECT_UPDATE_TIMEOUT', + }, }, buttons: { diff --git a/awx/ui/client/src/configuration/main.js b/awx/ui/client/src/configuration/main.js index 74dce80c8f..f37bab3d84 100644 --- a/awx/ui/client/src/configuration/main.js +++ b/awx/ui/client/src/configuration/main.js @@ -20,8 +20,12 @@ import configurationLdapForm from './auth-form/sub-forms/auth-ldap.form.js'; import configurationRadiusForm from './auth-form/sub-forms/auth-radius.form.js'; import configurationSamlForm from './auth-form/sub-forms/auth-saml.form'; +//system sub-forms +import systemActivityStreamForm from './system-form/sub-forms/system-activity-stream.form.js'; +import systemLoggingForm from './system-form/sub-forms/system-logging.form.js'; +import systemMiscForm from './system-form/sub-forms/system-misc.form.js'; + import configurationJobsForm from './jobs-form/configuration-jobs.form'; -import configurationSystemForm from './system-form/configuration-system.form'; import configurationUiForm from './ui-form/configuration-ui.form'; export default @@ -36,10 +40,15 @@ angular.module('configuration', []) .factory('configurationLdapForm', configurationLdapForm) .factory('configurationRadiusForm', configurationRadiusForm) .factory('configurationSamlForm', configurationSamlForm) + //system forms + .factory('systemActivityStreamForm', systemActivityStreamForm) + .factory('systemLoggingForm', systemLoggingForm) + .factory('systemMiscForm', systemMiscForm) + //other forms .factory('ConfigurationJobsForm', configurationJobsForm) - .factory('ConfigurationSystemForm', configurationSystemForm) .factory('ConfigurationUiForm', configurationUiForm) + //helpers and services .factory('ConfigurationUtils', ConfigurationUtils) .service('ConfigurationService', configurationService) diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js index c22131c2bc..2774c8ade5 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js +++ b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js @@ -5,22 +5,120 @@ *************************************************/ export default [ - '$rootScope', '$scope', '$state', 'AngularCodeMirror', 'Authorization', 'ConfigurationSystemForm', 'ConfigurationService', - 'ConfigurationUtils', 'GenerateForm', + '$rootScope', '$scope', '$state', '$stateParams', + 'AngularCodeMirror', + 'systemActivityStreamForm', + 'systemLoggingForm', + 'systemMiscForm', + 'ConfigurationService', + 'ConfigurationUtils', + 'CreateSelect2', + 'GenerateForm', + 'i18n', function( - $rootScope, $scope, $state, AngularCodeMirror, Authorization, ConfigurationSystemForm, ConfigurationService, ConfigurationUtils, GenerateForm + $rootScope, $scope, $state, $stateParams, + AngularCodeMirror, + systemActivityStreamForm, + systemLoggingForm, + systemMiscForm, + ConfigurationService, + ConfigurationUtils, + CreateSelect2, + GenerateForm, + i18n ) { var systemVm = this; - var generator = GenerateForm; - var form = ConfigurationSystemForm; - var keys = _.keys(form.fields); - _.each(keys, function(key) { - addFieldInfo(form, key); + var generator = GenerateForm; + var formTracker = $scope.$parent.vm.formTracker; + var dropdownValue = 'misc'; + var activeSystemForm = 'misc'; + + if ($stateParams.currentTab === 'system') { + formTracker.setCurrentSystem(activeSystemForm); + } + + var activeForm = function() { + if(!$scope.$parent[formTracker.currentFormName()].$dirty) { + systemVm.activeSystemForm = systemVm.dropdownValue; + formTracker.setCurrentSystem(systemVm.activeSystemForm); + } else { + var msg = i18n._('You have unsaved changes. Would you like to proceed without saving?'); + var title = i18n._('Warning: Unsaved Changes'); + var buttons = [{ + label: i18n._('Discard changes'), + "class": "btn Form-cancelButton", + "id": "formmodal-cancel-button", + onClick: function() { + $scope.$parent.vm.populateFromApi(); + $scope.$parent[formTracker.currentFormName()].$setPristine(); + systemVm.activeSystemForm = systemVm.dropdownValue; + formTracker.setCurrentSystem(systemVm.activeSystemForm); + $('#FormModal-dialog').dialog('close'); + } + }, { + label: i18n._('Save changes'), + onClick: function() { + $scope.$parent.vm.formSave() + .then(function() { + $scope.$parent[formTracker.currentFormName()].$setPristine(); + $scope.$parent.vm.populateFromApi(); + systemVm.activeSystemForm = systemVm.dropdownValue; + formTracker.setCurrentSystem(systemVm.activeSystemForm); + $('#FormModal-dialog').dialog('close'); + }); + }, + "class": "btn btn-primary", + "id": "formmodal-save-button" + }]; + $scope.$parent.vm.triggerModal(msg, title, buttons); + } + formTracker.setCurrentSystem(systemVm.activeSystemForm); + }; + + var dropdownOptions = [ + {label: i18n._('Misc. System'), value: 'misc'}, + {label: i18n._('Activity Stream'), value: 'activity_stream'}, + {label: i18n._('Logging'), value: 'logging'}, + ]; + + CreateSelect2({ + element: '#system-configure-dropdown-nav', + multiple: false, }); - // Disable the save button for system auditors - form.buttons.save.disabled = $rootScope.user_is_system_auditor; + var systemForms = [{ + formDef: systemLoggingForm, + id: 'system-logging-form' + }, { + formDef: systemActivityStreamForm, + id: 'system-activity-stream-form' + }, { + formDef: systemMiscForm, + id: 'system-misc-form' + }]; + + var forms = _.pluck(systemForms, 'formDef'); + _.each(forms, function(form) { + var keys = _.keys(form.fields); + _.each(keys, function(key) { + if($scope.$parent.configDataResolve[key].type === 'choice') { + // Create options for dropdowns + var optionsGroup = key + '_options'; + $scope.$parent[optionsGroup] = []; + _.each($scope.$parent.configDataResolve[key].choices, function(choice){ + $scope.$parent[optionsGroup].push({ + name: choice[0], + label: choice[1], + value: choice[0] + }); + }); + } + addFieldInfo(form, key); + }); + // Disable the save button for system auditors + form.buttons.save.disabled = $rootScope.user_is_system_auditor; + }); function addFieldInfo(form, key) { _.extend(form.fields[key], { @@ -29,21 +127,56 @@ export default [ name: key, toggleSource: key, dataPlacement: 'top', + placeholder: ConfigurationUtils.formatPlaceholder($scope.$parent.configDataResolve[key].placeholder, key) || null, dataTitle: $scope.$parent.configDataResolve[key].label, required: $scope.$parent.configDataResolve[key].required, ngDisabled: $rootScope.user_is_system_auditor }); } - generator.inject(form, { - id: 'configure-system-form', - mode: 'edit', - scope: $scope.$parent, - related: true + $scope.$parent.parseType = 'json'; + + _.each(systemForms, function(form) { + generator.inject(form.formDef, { + id: form.id, + mode: 'edit', + scope: $scope.$parent, + related: true + }); + }); + + var dropdownRendered = false; + + $scope.$on('populated', function() { + + var opts = []; + if($scope.$parent.LOG_AGGREGATOR_TYPE !== null) { + _.each(ConfigurationUtils.listToArray($scope.$parent.LOG_AGGREGATOR_TYPE), function(type) { + opts.push({ + id: type, + text: type + }); + }); + } + + if(!dropdownRendered) { + dropdownRendered = true; + CreateSelect2({ + element: '#configuration_logging_template_LOG_AGGREGATOR_TYPE', + multiple: true, + placeholder: i18n._('Select types'), + opts: opts + }); + } + }); angular.extend(systemVm, { - + activeForm: activeForm, + activeSystemForm: activeSystemForm, + dropdownOptions: dropdownOptions, + dropdownValue: dropdownValue, + systemForms: systemForms }); } ]; diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.partial.html b/awx/ui/client/src/configuration/system-form/configuration-system.partial.html index 7ff91b4441..0b039e761b 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.partial.html +++ b/awx/ui/client/src/configuration/system-form/configuration-system.partial.html @@ -1,9 +1,34 @@
- +
+
Sub Category
+
+ +
+
-
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
diff --git a/awx/ui/client/src/configuration/system-form/sub-forms/system-activity-stream.form.js b/awx/ui/client/src/configuration/system-form/sub-forms/system-activity-stream.form.js new file mode 100644 index 0000000000..09cf80eccd --- /dev/null +++ b/awx/ui/client/src/configuration/system-form/sub-forms/system-activity-stream.form.js @@ -0,0 +1,38 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default ['i18n', function(i18n) { + return { + name: 'configuration_activity_stream_template', + showActions: true, + showHeader: false, + + fields: { + ACTIVITY_STREAM_ENABLED: { + type: 'toggleSwitch', + }, + ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: { + type: 'toggleSwitch' + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: i18n._('Reset All'), + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; + } +]; diff --git a/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js b/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js new file mode 100644 index 0000000000..2329fde488 --- /dev/null +++ b/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js @@ -0,0 +1,63 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default ['i18n', function(i18n) { + return { + name: 'configuration_logging_template', + showActions: true, + showHeader: false, + + fields: { + LOG_AGGREGATOR_HOST: { + type: 'text', + reset: 'LOG_AGGREGATOR_HOST' + }, + LOG_AGGREGATOR_PORT: { + type: 'text', + reset: 'LOG_AGGREGATOR_PORT' + }, + LOG_AGGREGATOR_TYPE: { + type: 'select', + reset: 'LOG_AGGREGATOR_TYPE', + ngOptions: 'type.label for type in LOG_AGGREGATOR_TYPE_options track by type.value', + }, + LOG_AGGREGATOR_USERNAME: { + type: 'text', + reset: 'LOG_AGGREGATOR_USERNAME' + }, + LOG_AGGREGATOR_PASSWORD: { + type: 'text', + reset: 'LOG_AGGREGATOR_PASSWORD' + }, + LOG_AGGREGATOR_LOGGERS: { + type: 'textarea', + reset: 'LOG_AGGREGATOR_PASSWORD' + }, + LOG_AGGREGATOR_INDIVIDUAL_FACTS: { + type: 'toggleSwitch', + }, + LOG_AGGREGATOR_ENABLED: { + type: 'toggleSwitch', + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: i18n._('Reset All'), + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; + } +]; diff --git a/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js b/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js new file mode 100644 index 0000000000..690418f323 --- /dev/null +++ b/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js @@ -0,0 +1,42 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['i18n', function(i18n) { + return { + showHeader: false, + name: 'configuration_misc_template', + showActions: true, + + fields: { + TOWER_URL_BASE: { + type: 'text', + reset: 'TOWER_URL_BASE', + }, + TOWER_ADMIN_ALERTS: { + type: 'toggleSwitch', + }, + ORG_ADMINS_CAN_SEE_ALL_USERS: { + type: 'toggleSwitch', + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: i18n._('Reset All'), + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; +} +]; From c4c5e674274d0e2237d2f70131abcf85ac22be54 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 14 Dec 2016 10:16:33 -0500 Subject: [PATCH 131/595] Properly redirect after creating a new credential --- awx/ui/client/src/helpers/Credentials.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/helpers/Credentials.js b/awx/ui/client/src/helpers/Credentials.js index a58c37bce8..ee7d187c39 100644 --- a/awx/ui/client/src/helpers/Credentials.js +++ b/awx/ui/client/src/helpers/Credentials.js @@ -289,14 +289,12 @@ angular.module('CredentialsHelper', ['Utilities']) Wait('stop'); var base = $location.path().replace(/^\//, '').split('/')[0]; - if (base === 'credentials') { - ReturnToCaller(); + if (base === 'credentials') { + $state.go('credentials.edit', {credential_id: data.id}, {reload: true}); } else { ReturnToCaller(1); } - $state.go('credentials.edit', {credential_id: data.id}, {reload: true}); - }) .error(function (data, status) { Wait('stop'); From b533a9eb7b5561af07eaa0dbb0c20110e37ccbc0 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 14 Dec 2016 10:30:28 -0500 Subject: [PATCH 132/595] purge make requirements_jenkins --- Makefile | 11 +---------- requirements/requirements_jenkins.txt | 13 ------------- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 requirements/requirements_jenkins.txt diff --git a/Makefile b/Makefile index 684b267244..89263181bc 100644 --- a/Makefile +++ b/Makefile @@ -175,7 +175,6 @@ UI_RELEASE_FLAG_FILE = awx/ui/.release_built .DEFAULT_GOAL := build .PHONY: clean clean-tmp clean-venv rebase push requirements requirements_dev \ - requirements_jenkins \ develop refresh adduser migrate dbchange dbshell runserver celeryd \ receiver test test_unit test_coverage coverage_html test_jenkins dev_build \ release_build release_clean sdist rpmtar mock-rpm mock-srpm rpm-sign \ @@ -309,19 +308,11 @@ requirements_tower_dev: $(VENV_BASE)/tower/bin/pip uninstall --yes -r requirements/requirements_dev_uninstall.txt; \ fi -# Install third-party requirements needed for running unittests in jenkins -requirements_jenkins: - if [ "$(VENV_BASE)" ]; then \ - . $(VENV_BASE)/tower/bin/activate && pip install -Ir requirements/requirements_jenkins.txt; \ - else \ - pip install -Ir requirements/requirements_jenkins.txt; \ - fi - requirements: requirements_ansible requirements_tower requirements_dev: requirements requirements_tower_dev -requirements_test: requirements requirements_jenkins +requirements_test: requirements # "Install" ansible-tower package in development mode. develop: diff --git a/requirements/requirements_jenkins.txt b/requirements/requirements_jenkins.txt deleted file mode 100644 index 1546b0ae3b..0000000000 --- a/requirements/requirements_jenkins.txt +++ /dev/null @@ -1,13 +0,0 @@ -ansible==1.9.4 -coverage -pyflakes -pep8 -pylint -flake8 -distribute==0.7.3 -unittest2 -pytest==2.9.2 -pytest-cov -pytest-django -pytest-pythonpath -pytest-mock From b947367606217c7de81f46b0af6645c931d37d14 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 14 Dec 2016 11:54:21 -0500 Subject: [PATCH 133/595] Add default for ldap group type. --- awx/sso/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/sso/conf.py b/awx/sso/conf.py index c3ab7f7e56..237209a017 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -331,6 +331,7 @@ register( category=_('LDAP'), category_slug='ldap', feature_required='ldap', + default='MemberDNGroupType', ) register( From 13798d352c33d71161bdcd38320e1420911271b4 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Wed, 14 Dec 2016 12:49:50 -0500 Subject: [PATCH 134/595] use DjangoJSONEncoder --- awx/main/consumers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/consumers.py b/awx/main/consumers.py index a8c56a264d..2cb1f450f2 100644 --- a/awx/main/consumers.py +++ b/awx/main/consumers.py @@ -6,6 +6,7 @@ from channels import Group from channels.sessions import channel_session from django.contrib.auth.models import User +from django.core.serializers.json import DjangoJSONEncoder from awx.main.models.organization import AuthToken @@ -86,4 +87,4 @@ def ws_receive(message): def emit_channel_notification(group, payload): - Group(group).send({"text": json.dumps(payload)}) + Group(group).send({"text": json.dumps(payload, cls=DjangoJSONEncoder)}) From b9aab38185212a3e215db2743a3defa1ad75a17b Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 14 Dec 2016 13:20:08 -0500 Subject: [PATCH 135/595] Handle TypeError when lookup is not valid for a given field. --- awx/api/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/filters.py b/awx/api/filters.py index 5146ff0cd2..6885d73326 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -242,7 +242,7 @@ class FieldLookupBackend(BaseFilterBackend): queryset = queryset.filter(q) queryset = queryset.filter(*args).distinct() return queryset - except (FieldError, FieldDoesNotExist, ValueError) as e: + except (FieldError, FieldDoesNotExist, ValueError, TypeError) as e: raise ParseError(e.args[0]) except ValidationError as e: raise ParseError(e.messages) From 46e74a24f05b2cd97a5309ed9316fa9683cea96a Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 14 Dec 2016 10:27:22 -0800 Subject: [PATCH 136/595] adding status line to beginning of left side panel --- .../src/job-results/job-results.partial.html | 51 +++++-------------- .../workflow-results.partial.html | 20 +++++++- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 4b2d8be6ea..c7fd2a1e0d 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -62,6 +62,19 @@
+ +
+ +
+ + {{ status_label }} +
+
+
@@ -387,44 +400,6 @@
- - - - - - -
diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index ac04b114e8..86f0ad13a4 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -62,6 +62,19 @@
+ +
+ +
+ + {{ status_label }} +
+
+
@@ -180,7 +193,7 @@
- +
@@ -195,7 +208,10 @@
+ fa icon-job-{{ workflow.status }}" + aw-tool-tip="Job {{status_label}}" + aw-tip-placement="top" + data-original-title> {{ workflow.name }}
From 0a6d2f179e2a33555cb1be69777b90ab5d76bec8 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 14 Dec 2016 13:50:25 -0500 Subject: [PATCH 137/595] Extend stdout background to width of text. --- awx/static/api/api.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/static/api/api.css b/awx/static/api/api.css index 61d51fae12..3b18c4273d 100644 --- a/awx/static/api/api.css +++ b/awx/static/api/api.css @@ -151,6 +151,9 @@ body .prettyprint .lit { body .prettyprint .str { color: #D9534F; } +body div.ansi_back { + display: inline-block; +} body .well.tab-content { padding: 20px; From cb83069c67df96066aa6552aa1d694495211d98e Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 14 Dec 2016 15:03:20 -0500 Subject: [PATCH 138/595] fixed on reload job status' --- awx/ui/client/src/job-results/job-results.controller.js | 8 ++++++-- awx/ui/client/src/job-results/job-results.partial.html | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index b2f83ede02..42f8d61425 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -1,4 +1,9 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', 'eventQueue', '$compile', '$log', 'Dataset', '$q', 'Rest', '$state', 'QuerySet', function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, eventQueue, $compile, $log, Dataset, $q, Rest, $state, QuerySet) { + // if the job_status hasn't been set by the websocket, set it now + if (!$scope.job_status) { + $scope.job_status = jobData.status; + } + // used for tag search $scope.job_event_dataset = Dataset.data; @@ -111,7 +116,6 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' $scope.relaunchJob = function() { jobResultsService.relaunchJob($scope); - $state.reload(); }; $scope.lessLabels = false; @@ -417,7 +421,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' $scope.$on(`ws-jobs`, function(e, data) { if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) { - $scope.job.status = data.status; + $scope.job_status = data.status; } if (parseInt(data.project_id, 10) === parseInt($scope.job.project,10)) { diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index b12c7af487..77204b8d73 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -490,7 +490,7 @@
+ fa icon-job-{{ job_status }}"> {{ job.name }}
From 1a9f304b7731d4a8967ba132a86a1144c8d83976 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 14 Dec 2016 15:05:14 -0500 Subject: [PATCH 139/595] fix extra vars labels --- awx/ui/client/src/partials/logviewer.html | 2 +- .../management-jobs/standard-out-management-jobs.partial.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/partials/logviewer.html b/awx/ui/client/src/partials/logviewer.html index ff6288d2db..59fe6ca045 100644 --- a/awx/ui/client/src/partials/logviewer.html +++ b/awx/ui/client/src/partials/logviewer.html @@ -5,7 +5,7 @@
  • Standard Out
  • Traceback
  • Options
  • -
  • Extra Vars
  • +
  • Extra Variables
  • Source Vars
  • diff --git a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html index efb18d1b48..680c004f4d 100644 --- a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html +++ b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html @@ -56,7 +56,7 @@
    -
    EXTRA VARS
    +
    EXTRA VARIABLES
    From 0c7421460c126b2e7893e09bd9d36e054b1125a8 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 14 Dec 2016 15:05:26 -0500 Subject: [PATCH 140/595] fix padding after search tags --- awx/ui/client/src/shared/smart-search/smart-search.block.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/shared/smart-search/smart-search.block.less b/awx/ui/client/src/shared/smart-search/smart-search.block.less index 23afbc7791..0b22331d5a 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.block.less +++ b/awx/ui/client/src/shared/smart-search/smart-search.block.less @@ -100,7 +100,7 @@ font-size: 12px; color: @default-interface-txt; background-color: @default-bg; - margin-right: 5px; + margin-right: 10px; max-width: 100%; white-space: nowrap; text-overflow: ellipsis; @@ -115,7 +115,7 @@ max-width: ~"calc(100% - 23px)"; background-color: @default-link; color: @default-bg; - margin-right: 5px; + margin-right: 10px; } .SmartSearch-deleteContainer { From a61e729ebbc27a85e492010f3571cb659c8f8762 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 14 Dec 2016 15:05:28 -0500 Subject: [PATCH 141/595] Purge event res dict if it is over a certain size Also purge/update some old settings values --- awx/lib/tower_display_callback/events.py | 4 +++- awx/main/tasks.py | 1 + awx/settings/defaults.py | 17 +++++------------ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/awx/lib/tower_display_callback/events.py b/awx/lib/tower_display_callback/events.py index 0909ed460d..c17cf2c7f1 100644 --- a/awx/lib/tower_display_callback/events.py +++ b/awx/lib/tower_display_callback/events.py @@ -173,7 +173,9 @@ class EventContext(object): if event_data.get(key, False): event = key break - + max_res = int(os.getenv("MAX_EVENT_RES", 700000)) + if event not in ('playbook_on_stats',) and "res" in event_data and len(str(event_data['res'])) > max_res: + event_data['res'] = {} event_dict = dict(event=event, event_data=event_data) for key in event_data.keys(): if key in ('job_id', 'ad_hoc_command_id', 'uuid', 'parent_uuid', 'created', 'artifact_data'): diff --git a/awx/main/tasks.py b/awx/main/tasks.py index e0dcae0fca..addbe4c8f2 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -809,6 +809,7 @@ class RunJob(BaseTask): env['REST_API_URL'] = settings.INTERNAL_API_URL env['REST_API_TOKEN'] = job.task_auth_token or '' env['TOWER_HOST'] = settings.TOWER_URL_BASE + env['MAX_EVENT_RES'] = settings.MAX_EVENT_RES_DATA env['CALLBACK_QUEUE'] = settings.CALLBACK_QUEUE env['CALLBACK_CONNECTION'] = settings.BROKER_URL if getattr(settings, 'JOB_CALLBACK_DEBUG', False): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index fbb9c8fb73..9c758dfb13 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -150,7 +150,11 @@ ALLOWED_HOSTS = [] REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] # Note: This setting may be overridden by database settings. -STDOUT_MAX_BYTES_DISPLAY = 1048576 +STDOUT_MAX_BYTES_DISPLAY = 10485760 + +# The maximum size of the ansible callback event's res data structure +# beyond this limit and the value will be removed +MAX_EVENT_RES_DATA = 700000 # Note: This setting may be overridden by database settings. EVENT_STDOUT_MAX_BYTES_DISPLAY = 1024 @@ -522,17 +526,6 @@ ANSIBLE_FORCE_COLOR = True # the celery task. AWX_TASK_ENV = {} -# Maximum number of job events processed by the callback receiver worker process -# before it recycles -JOB_EVENT_RECYCLE_THRESHOLD = 3000 - -# Number of workers used to proecess job events in parallel -JOB_EVENT_WORKERS = 4 - -# Maximum number of job events that can be waiting on a single worker queue before -# it can be skipped as too busy -JOB_EVENT_MAX_QUEUE_SIZE = 100 - # Flag to enable/disable updating hosts M2M when saving job events. CAPTURE_JOB_EVENT_HOSTS = False From 2988ac19047eccc620f6807f81ab09fbe861823c Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 14 Dec 2016 15:12:27 -0500 Subject: [PATCH 142/595] Only use known stats keys for determining hostnames to use for job host summaries. --- awx/main/models/jobs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index b7acf21775..19b62c7694 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -1073,8 +1073,8 @@ class JobEvent(CreatedModifiedModel): from awx.main.models.inventory import Host hostnames = set() try: - for v in self.event_data.values(): - hostnames.update(v.keys()) + for stat in ('changed', 'dark', 'failures', 'ok', 'processed', 'skipped'): + hostnames.update(self.event_data.get(stat, {}).keys()) except AttributeError: # In case event_data or v isn't a dict. pass with ignore_inventory_computed_fields(): From 1cd2a762bec82a1da79def356684506dbc43d3f3 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 14 Dec 2016 15:20:04 -0500 Subject: [PATCH 143/595] Reset max bytes display --- awx/settings/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 9c758dfb13..876ba56c85 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -150,7 +150,7 @@ ALLOWED_HOSTS = [] REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] # Note: This setting may be overridden by database settings. -STDOUT_MAX_BYTES_DISPLAY = 10485760 +STDOUT_MAX_BYTES_DISPLAY = 1048576 # The maximum size of the ansible callback event's res data structure # beyond this limit and the value will be removed From 85264cb01adefc87c61866c1b89af8d8ef7911b5 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 14 Dec 2016 15:25:34 -0500 Subject: [PATCH 144/595] fix line anchoring of expand and collapse all --- .../job-results-stdout.directive.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js index 0d1674b267..77c87c74da 100644 --- a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js +++ b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js @@ -90,23 +90,14 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll', var visItem, parentItem; - var containerHeight = $container.height(); - var containerTop = $container.position().top; - var containerNetHeight = containerHeight + containerTop; - // iterate through each line of standard out - $container.find('.JobResultsStdOut-aLineOfStdOut') + $container.find('.JobResultsStdOut-aLineOfStdOut:visible') .each( function () { var $this = $(this); - var lineHeight = $this.height(); - var lineTop = $this.position().top; - var lineNetHeight = lineHeight + lineTop; - // check to see if the line is the first visible // line in the viewport... - if (lineNetHeight > containerTop && - lineTop < containerNetHeight) { + if ($this.position().top >= 0) { // ...if it is, return the line number // for this line From 912af09831f57c7f5be47ec698bcc316fe1e5645 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 14 Dec 2016 15:35:04 -0500 Subject: [PATCH 145/595] Fixed organization add user/admin save button functionality --- .../linkout/addUsers/addUsers.controller.js | 23 +++++++++++++++---- .../organizations-admins.controller.js | 2 +- .../organizations-users.controller.js | 2 +- .../linkout/organizations-linkout.route.js | 3 +++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js index b42d143b00..b45797a70b 100644 --- a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js +++ b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js @@ -12,9 +12,9 @@ */ export default ['$scope', '$rootScope', 'ProcessErrors', 'GetBasePath', -'SelectionInit', 'templateUrl', '$state', 'Rest', '$q', 'Wait', +'SelectionInit', 'templateUrl', '$state', 'Rest', '$q', 'Wait', '$window', function($scope, $rootScope, ProcessErrors, GetBasePath, - SelectionInit, templateUrl, $state, Rest, $q, Wait) { + SelectionInit, templateUrl, $state, Rest, $q, Wait, $window) { $scope.$on("linkLists", function() { if ($state.current.name.split(".")[1] === "users") { @@ -32,8 +32,15 @@ function($scope, $rootScope, ProcessErrors, GetBasePath, $scope.add_users = $scope.$parent.add_user_dataset.results; $scope.selectedItems = []; - $scope.$on('selectedOrDeselected', ()=>{ - throw {name: 'NotYetImplemented'}; + $scope.$on('selectedOrDeselected', function(e, value) { + let item = value.value; + + if (item.isSelected) { + $scope.selectedItems.push(item.id); + } + else { + $scope.selectedItems = _.remove($scope.selectedItems, { id: item.id }); + } }); } @@ -42,7 +49,7 @@ function($scope, $rootScope, ProcessErrors, GetBasePath, var url, listToClose, payloads = $scope.selectedItems.map(function(val) { - return {id: val.id}; + return {id: val}; }); url = $scope.$parent.orgRelatedUrls[$scope.addUsersType]; @@ -69,5 +76,11 @@ function($scope, $rootScope, ProcessErrors, GetBasePath, }); }); }; + + $scope.linkoutUser = function(userId) { + // Open the edit user form in a new tab so as not to navigate the user + // away from the modal + $window.open('/#/users/' + userId,'_blank'); + }; }); }]; diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-admins.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-admins.controller.js index dc8c5b0c12..df3075305e 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-admins.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-admins.controller.js @@ -37,7 +37,7 @@ export default ['$stateParams', '$scope', 'UserList', 'Rest', '$state', } $scope.addUsers = function() { - $compile("")($scope); + $compile("")($scope); }; $scope.editUser = function(id) { diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-users.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-users.controller.js index 8baede4f2e..2f3e0f7872 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-users.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-users.controller.js @@ -37,7 +37,7 @@ export default ['$stateParams', '$scope', 'OrgUserList', 'AddUserList','Rest', ' } $scope.addUsers = function() { - $compile("")($scope); + $compile("")($scope); }; $scope.editUser = function(id) { diff --git a/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js b/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js index 3f65c144ab..d8efd0c813 100644 --- a/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js +++ b/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js @@ -99,6 +99,7 @@ export default [{ list.iterator = 'add_user'; list.name = 'add_users'; list.multiSelect = true; + list.fields.username.ngClick = 'linkoutUser(add_user.id)'; delete list.actions; delete list.fieldActions; return list; @@ -386,6 +387,7 @@ export default [{ } }; list.searchSize = "col-lg-12 col-md-12 col-sm-12 col-xs-12"; + list.listTitle = 'Admins'; return list; }], AddAdminList: ['UserList', function(UserList) { @@ -394,6 +396,7 @@ export default [{ list.iterator = 'add_user'; list.name = 'add_users'; list.multiSelect = true; + list.fields.username.ngClick = 'linkoutUser(add_user.id)'; delete list.actions; delete list.fieldActions; return list; From a25853a3a53b30bd138e334a7b47adcc40e09f68 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 14 Dec 2016 15:57:40 -0500 Subject: [PATCH 146/595] Updating translation artifacts * also installing translation tools into dev environment * Removing fedora install instructions, no one here uses fedora --- awx/locale/django.pot | 1801 ++++++++++++++------------ awx/ui/po/ansible-tower-ui.pot | 291 +++-- tools/docker-compose/Dockerfile | 2 +- tools/scripts/manage_translations.py | 5 +- 4 files changed, 1179 insertions(+), 920 deletions(-) mode change 100644 => 100755 tools/scripts/manage_translations.py diff --git a/awx/locale/django.pot b/awx/locale/django.pot index 7b67733c8b..81993a2a2b 100644 --- a/awx/locale/django.pot +++ b/awx/locale/django.pot @@ -1,12 +1,14 @@ -# Ansible Tower POT file. -# Copyright (c) 2016 Ansible, Inc. -# All Rights Reserved. +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. # +#, fuzzy msgid "" msgstr "" -"Project-Id-Version: ansible-tower-3.1.0\n" +"Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-12-01 06:37-0500\n" +"POT-Creation-Date: 2016-12-14 19:21+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -15,1033 +17,1039 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: awx/api/authentication.py:67 +#: api/authentication.py:67 msgid "Invalid token header. No credentials provided." msgstr "" -#: awx/api/authentication.py:70 +#: api/authentication.py:70 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: awx/api/authentication.py:105 +#: api/authentication.py:105 msgid "User inactive or deleted" msgstr "" -#: awx/api/authentication.py:161 +#: api/authentication.py:161 msgid "Invalid task token" msgstr "" -#: awx/api/conf.py:12 +#: api/conf.py:12 msgid "Idle Time Force Log Out" msgstr "" -#: awx/api/conf.py:13 +#: api/conf.py:13 msgid "" "Number of seconds that a user is inactive before they will need to login " "again." msgstr "" -#: awx/api/conf.py:14 awx/api/conf.py:24 awx/api/conf.py:33 awx/sso/conf.py:124 -#: awx/sso/conf.py:135 awx/sso/conf.py:147 awx/sso/conf.py:162 +#: api/conf.py:14 api/conf.py:24 api/conf.py:33 sso/conf.py:124 +#: sso/conf.py:135 sso/conf.py:147 sso/conf.py:162 msgid "Authentication" msgstr "" -#: awx/api/conf.py:22 +#: api/conf.py:22 msgid "Maximum number of simultaneous logins" msgstr "" -#: awx/api/conf.py:23 +#: api/conf.py:23 msgid "" "Maximum number of simultaneous logins a user may have. To disable enter -1." msgstr "" -#: awx/api/conf.py:31 +#: api/conf.py:31 msgid "Enable HTTP Basic Auth" msgstr "" -#: awx/api/conf.py:32 +#: api/conf.py:32 msgid "Enable HTTP Basic Auth for the API Browser." msgstr "" -#: awx/api/generics.py:446 +#: api/generics.py:446 msgid "\"id\" is required to disassociate" msgstr "" -#: awx/api/metadata.py:50 +#: api/metadata.py:50 msgid "Database ID for this {}." msgstr "" -#: awx/api/metadata.py:51 +#: api/metadata.py:51 msgid "Name of this {}." msgstr "" -#: awx/api/metadata.py:52 +#: api/metadata.py:52 msgid "Optional description of this {}." msgstr "" -#: awx/api/metadata.py:53 +#: api/metadata.py:53 msgid "Data type for this {}." msgstr "" -#: awx/api/metadata.py:54 +#: api/metadata.py:54 msgid "URL for this {}." msgstr "" -#: awx/api/metadata.py:55 +#: api/metadata.py:55 msgid "Data structure with URLs of related resources." msgstr "" -#: awx/api/metadata.py:56 +#: api/metadata.py:56 msgid "Data structure with name/description for related resources." msgstr "" -#: awx/api/metadata.py:57 +#: api/metadata.py:57 msgid "Timestamp when this {} was created." msgstr "" -#: awx/api/metadata.py:58 +#: api/metadata.py:58 msgid "Timestamp when this {} was last modified." msgstr "" -#: awx/api/parsers.py:31 +#: api/parsers.py:31 #, python-format msgid "JSON parse error - %s" msgstr "" -#: awx/api/serializers.py:248 +#: api/serializers.py:248 msgid "Playbook Run" msgstr "" -#: awx/api/serializers.py:249 +#: api/serializers.py:249 msgid "Command" msgstr "" -#: awx/api/serializers.py:250 +#: api/serializers.py:250 msgid "SCM Update" msgstr "" -#: awx/api/serializers.py:251 +#: api/serializers.py:251 msgid "Inventory Sync" msgstr "" -#: awx/api/serializers.py:252 +#: api/serializers.py:252 msgid "Management Job" msgstr "" -#: awx/api/serializers.py:636 awx/api/serializers.py:694 awx/api/views.py:3994 +#: api/serializers.py:253 +msgid "Workflow Job" +msgstr "" + +#: api/serializers.py:655 api/serializers.py:713 api/views.py:3914 #, python-format msgid "" "Standard Output too large to display (%(text_size)d bytes), only download " "supported for sizes over %(supported_size)d bytes" msgstr "" -#: awx/api/serializers.py:709 +#: api/serializers.py:728 msgid "Write-only field used to change the password." msgstr "" -#: awx/api/serializers.py:711 +#: api/serializers.py:730 msgid "Set if the account is managed by an external service" msgstr "" -#: awx/api/serializers.py:735 +#: api/serializers.py:754 msgid "Password required for new User." msgstr "" -#: awx/api/serializers.py:819 +#: api/serializers.py:838 #, python-format msgid "Unable to change %s on user managed by LDAP." msgstr "" -#: awx/api/serializers.py:971 +#: api/serializers.py:990 msgid "Organization is missing" msgstr "" -#: awx/api/serializers.py:977 +#: api/serializers.py:996 msgid "Array of playbooks available within this project." msgstr "" -#: awx/api/serializers.py:1159 +#: api/serializers.py:1178 #, python-format msgid "Invalid port specification: %s" msgstr "" -#: awx/api/serializers.py:1187 awx/main/validators.py:192 +#: api/serializers.py:1206 main/validators.py:192 msgid "Must be valid JSON or YAML." msgstr "" -#: awx/api/serializers.py:1244 +#: api/serializers.py:1263 msgid "Invalid group name." msgstr "" -#: awx/api/serializers.py:1319 +#: api/serializers.py:1338 msgid "" "Script must begin with a hashbang sequence: i.e.... #!/usr/bin/env python" msgstr "" -#: awx/api/serializers.py:1372 +#: api/serializers.py:1391 msgid "If 'source' is 'custom', 'source_script' must be provided." msgstr "" -#: awx/api/serializers.py:1376 +#: api/serializers.py:1395 msgid "" "The 'source_script' does not belong to the same organization as the " "inventory." msgstr "" -#: awx/api/serializers.py:1378 +#: api/serializers.py:1397 msgid "'source_script' doesn't exist." msgstr "" -#: awx/api/serializers.py:1737 +#: api/serializers.py:1756 msgid "" "Write-only field used to add user to owner role. If provided, do not give " "either team or organization. Only valid for creation." msgstr "" -#: awx/api/serializers.py:1742 +#: api/serializers.py:1761 msgid "" "Write-only field used to add team to owner role. If provided, do not give " "either user or organization. Only valid for creation." msgstr "" -#: awx/api/serializers.py:1747 +#: api/serializers.py:1766 msgid "" -"Write-only field used to add organization to owner role. If provided, do not " -"give either team or team. Only valid for creation." +"Inherit permissions from organization roles. If provided on creation, do not " +"give either user or team." msgstr "" -#: awx/api/serializers.py:1763 +#: api/serializers.py:1782 msgid "Missing 'user', 'team', or 'organization'." msgstr "" -#: awx/api/serializers.py:1776 +#: api/serializers.py:1795 msgid "" "Credential organization must be set and match before assigning to a team" msgstr "" -#: awx/api/serializers.py:1868 +#: api/serializers.py:1887 msgid "This field is required." msgstr "" -#: awx/api/serializers.py:1870 awx/api/serializers.py:1872 +#: api/serializers.py:1889 api/serializers.py:1891 msgid "Playbook not found for project." msgstr "" -#: awx/api/serializers.py:1874 +#: api/serializers.py:1893 msgid "Must select playbook for project." msgstr "" -#: awx/api/serializers.py:1938 awx/main/models/jobs.py:279 +#: api/serializers.py:1957 main/models/jobs.py:280 msgid "Scan jobs must be assigned a fixed inventory." msgstr "" -#: awx/api/serializers.py:1940 awx/main/models/jobs.py:282 +#: api/serializers.py:1959 main/models/jobs.py:283 msgid "Job types 'run' and 'check' must have assigned a project." msgstr "" -#: awx/api/serializers.py:1943 +#: api/serializers.py:1962 msgid "Survey Enabled cannot be used with scan jobs." msgstr "" -#: awx/api/serializers.py:2005 +#: api/serializers.py:2024 msgid "Invalid job template." msgstr "" -#: awx/api/serializers.py:2090 +#: api/serializers.py:2109 msgid "Credential not found or deleted." msgstr "" -#: awx/api/serializers.py:2092 +#: api/serializers.py:2111 msgid "Job Template Project is missing or undefined." msgstr "" -#: awx/api/serializers.py:2094 +#: api/serializers.py:2113 msgid "Job Template Inventory is missing or undefined." msgstr "" -#: awx/api/serializers.py:2379 +#: api/serializers.py:2398 #, python-format msgid "%(job_type)s is not a valid job type. The choices are %(choices)s." msgstr "" -#: awx/api/serializers.py:2384 +#: api/serializers.py:2403 msgid "Workflow job template is missing during creation." msgstr "" -#: awx/api/serializers.py:2389 +#: api/serializers.py:2408 #, python-format msgid "Cannot nest a %s inside a WorkflowJobTemplate" msgstr "" -#: awx/api/serializers.py:2625 +#: api/serializers.py:2646 #, python-format msgid "Job Template '%s' is missing or undefined." msgstr "" -#: awx/api/serializers.py:2651 +#: api/serializers.py:2672 msgid "Must be a valid JSON or YAML dictionary." msgstr "" -#: awx/api/serializers.py:2796 +#: api/serializers.py:2817 msgid "" "Missing required fields for Notification Configuration: notification_type" msgstr "" -#: awx/api/serializers.py:2819 +#: api/serializers.py:2840 msgid "No values specified for field '{}'" msgstr "" -#: awx/api/serializers.py:2824 +#: api/serializers.py:2845 msgid "Missing required fields for Notification Configuration: {}." msgstr "" -#: awx/api/serializers.py:2827 +#: api/serializers.py:2848 msgid "Configuration field '{}' incorrect type, expected {}." msgstr "" -#: awx/api/serializers.py:2880 +#: api/serializers.py:2901 msgid "Inventory Source must be a cloud resource." msgstr "" -#: awx/api/serializers.py:2882 +#: api/serializers.py:2903 msgid "Manual Project can not have a schedule set." msgstr "" -#: awx/api/serializers.py:2904 +#: api/serializers.py:2925 msgid "DTSTART required in rrule. Value should match: DTSTART:YYYYMMDDTHHMMSSZ" msgstr "" -#: awx/api/serializers.py:2906 +#: api/serializers.py:2927 msgid "Multiple DTSTART is not supported." msgstr "" -#: awx/api/serializers.py:2908 +#: api/serializers.py:2929 msgid "RRULE require in rrule." msgstr "" -#: awx/api/serializers.py:2910 +#: api/serializers.py:2931 msgid "Multiple RRULE is not supported." msgstr "" -#: awx/api/serializers.py:2912 +#: api/serializers.py:2933 msgid "INTERVAL required in rrule." msgstr "" -#: awx/api/serializers.py:2914 +#: api/serializers.py:2935 msgid "TZID is not supported." msgstr "" -#: awx/api/serializers.py:2916 +#: api/serializers.py:2937 msgid "SECONDLY is not supported." msgstr "" -#: awx/api/serializers.py:2918 +#: api/serializers.py:2939 msgid "Multiple BYMONTHDAYs not supported." msgstr "" -#: awx/api/serializers.py:2920 +#: api/serializers.py:2941 msgid "Multiple BYMONTHs not supported." msgstr "" -#: awx/api/serializers.py:2922 +#: api/serializers.py:2943 msgid "BYDAY with numeric prefix not supported." msgstr "" -#: awx/api/serializers.py:2924 +#: api/serializers.py:2945 msgid "BYYEARDAY not supported." msgstr "" -#: awx/api/serializers.py:2926 +#: api/serializers.py:2947 msgid "BYWEEKNO not supported." msgstr "" -#: awx/api/serializers.py:2930 +#: api/serializers.py:2951 msgid "COUNT > 999 is unsupported." msgstr "" -#: awx/api/serializers.py:2934 +#: api/serializers.py:2955 msgid "rrule parsing failed validation." msgstr "" -#: awx/api/serializers.py:2952 +#: api/serializers.py:2973 msgid "" "A summary of the new and changed values when an object is created, updated, " "or deleted" msgstr "" -#: awx/api/serializers.py:2954 +#: api/serializers.py:2975 msgid "" "For create, update, and delete events this is the object type that was " "affected. For associate and disassociate events this is the object type " "associated or disassociated with object2." msgstr "" -#: awx/api/serializers.py:2957 +#: api/serializers.py:2978 msgid "" "Unpopulated for create, update, and delete events. For associate and " "disassociate events this is the object type that object1 is being associated " "with." msgstr "" -#: awx/api/serializers.py:2960 +#: api/serializers.py:2981 msgid "The action taken with respect to the given object(s)." msgstr "" -#: awx/api/serializers.py:3060 +#: api/serializers.py:3081 msgid "Unable to login with provided credentials." msgstr "" -#: awx/api/serializers.py:3062 +#: api/serializers.py:3083 msgid "Must include \"username\" and \"password\"." msgstr "" -#: awx/api/views.py:95 awx/templates/rest_framework/api.html:28 +#: api/views.py:96 +msgid "Your license does not allow use of the activity stream." +msgstr "" + +#: api/views.py:106 +msgid "Your license does not permit use of system tracking." +msgstr "" + +#: api/views.py:116 +msgid "Your license does not allow use of workflows." +msgstr "" + +#: api/views.py:124 templates/rest_framework/api.html:28 msgid "REST API" msgstr "" -#: awx/api/views.py:102 awx/templates/rest_framework/api.html:4 +#: api/views.py:131 templates/rest_framework/api.html:4 msgid "Ansible Tower REST API" msgstr "" -#: awx/api/views.py:118 +#: api/views.py:147 msgid "Version 1" msgstr "" -#: awx/api/views.py:169 +#: api/views.py:198 msgid "Ping" msgstr "" -#: awx/api/views.py:198 awx/conf/apps.py:10 +#: api/views.py:227 conf/apps.py:12 msgid "Configuration" msgstr "" -#: awx/api/views.py:248 +#: api/views.py:280 msgid "Invalid license data" msgstr "" -#: awx/api/views.py:250 +#: api/views.py:282 msgid "Missing 'eula_accepted' property" msgstr "" -#: awx/api/views.py:254 +#: api/views.py:286 msgid "'eula_accepted' value is invalid" msgstr "" -#: awx/api/views.py:257 +#: api/views.py:289 msgid "'eula_accepted' must be True" msgstr "" -#: awx/api/views.py:263 +#: api/views.py:296 msgid "Invalid JSON" msgstr "" -#: awx/api/views.py:270 +#: api/views.py:304 msgid "Invalid License" msgstr "" -#: awx/api/views.py:278 +#: api/views.py:314 msgid "Invalid license" msgstr "" -#: awx/api/views.py:289 +#: api/views.py:322 #, python-format msgid "Failed to remove license (%s)" msgstr "" -#: awx/api/views.py:294 +#: api/views.py:327 msgid "Dashboard" msgstr "" -#: awx/api/views.py:400 +#: api/views.py:433 msgid "Dashboard Jobs Graphs" msgstr "" -#: awx/api/views.py:436 +#: api/views.py:469 #, python-format msgid "Unknown period \"%s\"" msgstr "" -#: awx/api/views.py:450 +#: api/views.py:483 msgid "Schedules" msgstr "" -#: awx/api/views.py:469 +#: api/views.py:502 msgid "Schedule Jobs List" msgstr "" -#: awx/api/views.py:675 +#: api/views.py:711 msgid "Your Tower license only permits a single organization to exist." msgstr "" -#: awx/api/views.py:803 awx/api/views.py:968 awx/api/views.py:1069 -#: awx/api/views.py:1356 awx/api/views.py:1517 awx/api/views.py:1614 -#: awx/api/views.py:1758 awx/api/views.py:1954 awx/api/views.py:2212 -#: awx/api/views.py:2528 awx/api/views.py:3121 awx/api/views.py:3194 -#: awx/api/views.py:3330 awx/api/views.py:3911 awx/api/views.py:4163 -#: awx/api/views.py:4180 -msgid "Your license does not allow use of the activity stream." -msgstr "" - -#: awx/api/views.py:906 awx/api/views.py:1270 +#: api/views.py:932 api/views.py:1284 msgid "Role 'id' field is missing." msgstr "" -#: awx/api/views.py:912 awx/api/views.py:4282 +#: api/views.py:938 api/views.py:4182 msgid "You cannot assign an Organization role as a child role for a Team." msgstr "" -#: awx/api/views.py:919 awx/api/views.py:4288 +#: api/views.py:942 api/views.py:4196 +msgid "You cannot grant system-level permissions to a team." +msgstr "" + +#: api/views.py:949 api/views.py:4188 msgid "" "You cannot grant credential access to a team when the Organization field " "isn't set, or belongs to a different organization" msgstr "" -#: awx/api/views.py:1018 +#: api/views.py:1039 msgid "Cannot delete project." msgstr "" -#: awx/api/views.py:1047 +#: api/views.py:1068 msgid "Project Schedules" msgstr "" -#: awx/api/views.py:1156 awx/api/views.py:2307 awx/api/views.py:3301 +#: api/views.py:1168 api/views.py:2252 api/views.py:3225 msgid "Cannot delete job resource when associated workflow job is running." msgstr "" -#: awx/api/views.py:1230 +#: api/views.py:1244 msgid "Me" msgstr "" -#: awx/api/views.py:1274 awx/api/views.py:4237 +#: api/views.py:1288 api/views.py:4137 msgid "You may not perform any action with your own admin_role." msgstr "" -#: awx/api/views.py:1280 awx/api/views.py:4241 +#: api/views.py:1294 api/views.py:4141 msgid "You may not change the membership of a users admin_role" msgstr "" -#: awx/api/views.py:1285 awx/api/views.py:4246 +#: api/views.py:1299 api/views.py:4146 msgid "" "You cannot grant credential access to a user not in the credentials' " "organization" msgstr "" -#: awx/api/views.py:1289 awx/api/views.py:4250 +#: api/views.py:1303 api/views.py:4150 msgid "You cannot grant private credential access to another user" msgstr "" -#: awx/api/views.py:1397 +#: api/views.py:1401 #, python-format msgid "Cannot change %s." msgstr "" -#: awx/api/views.py:1403 +#: api/views.py:1407 msgid "Cannot delete user." msgstr "" -#: awx/api/views.py:1559 +#: api/views.py:1553 msgid "Cannot delete inventory script." msgstr "" -#: awx/api/views.py:1777 -msgid "Your license does not permit use of system tracking." -msgstr "" - -#: awx/api/views.py:1824 +#: api/views.py:1788 msgid "Fact not found." msgstr "" -#: awx/api/views.py:2154 +#: api/views.py:2108 msgid "Inventory Source List" msgstr "" -#: awx/api/views.py:2182 +#: api/views.py:2136 msgid "Cannot delete inventory source." msgstr "" -#: awx/api/views.py:2190 +#: api/views.py:2144 msgid "Inventory Source Schedules" msgstr "" -#: awx/api/views.py:2229 +#: api/views.py:2173 msgid "Notification Templates can only be assigned when source is one of {}." msgstr "" -#: awx/api/views.py:2433 +#: api/views.py:2380 msgid "Job Template Schedules" msgstr "" -#: awx/api/views.py:2452 awx/api/views.py:2462 +#: api/views.py:2399 api/views.py:2409 msgid "Your license does not allow adding surveys." msgstr "" -#: awx/api/views.py:2469 +#: api/views.py:2416 msgid "'name' missing from survey spec." msgstr "" -#: awx/api/views.py:2471 +#: api/views.py:2418 msgid "'description' missing from survey spec." msgstr "" -#: awx/api/views.py:2473 +#: api/views.py:2420 msgid "'spec' missing from survey spec." msgstr "" -#: awx/api/views.py:2475 +#: api/views.py:2422 msgid "'spec' must be a list of items." msgstr "" -#: awx/api/views.py:2477 +#: api/views.py:2424 msgid "'spec' doesn't contain any items." msgstr "" -#: awx/api/views.py:2482 +#: api/views.py:2429 #, python-format msgid "Survey question %s is not a json object." msgstr "" -#: awx/api/views.py:2484 +#: api/views.py:2431 #, python-format msgid "'type' missing from survey question %s." msgstr "" -#: awx/api/views.py:2486 +#: api/views.py:2433 #, python-format msgid "'question_name' missing from survey question %s." msgstr "" -#: awx/api/views.py:2488 +#: api/views.py:2435 #, python-format msgid "'variable' missing from survey question %s." msgstr "" -#: awx/api/views.py:2490 +#: api/views.py:2437 #, python-format msgid "'variable' '%(item)s' duplicated in survey question %(survey)s." msgstr "" -#: awx/api/views.py:2495 +#: api/views.py:2442 #, python-format msgid "'required' missing from survey question %s." msgstr "" -#: awx/api/views.py:2702 +#: api/views.py:2641 msgid "No matching host could be found!" msgstr "" -#: awx/api/views.py:2705 +#: api/views.py:2644 msgid "Multiple hosts matched the request!" msgstr "" -#: awx/api/views.py:2710 +#: api/views.py:2649 msgid "Cannot start automatically, user input required!" msgstr "" -#: awx/api/views.py:2717 +#: api/views.py:2656 msgid "Host callback job already pending." msgstr "" -#: awx/api/views.py:2730 +#: api/views.py:2669 msgid "Error starting job!" msgstr "" -#: awx/api/views.py:3053 +#: api/views.py:2995 msgid "Workflow Job Template Schedules" msgstr "" -#: awx/api/views.py:3208 awx/api/views.py:3933 +#: api/views.py:3131 api/views.py:3853 msgid "Superuser privileges needed." msgstr "" -#: awx/api/views.py:3238 +#: api/views.py:3161 msgid "System Job Template Schedules" msgstr "" -#: awx/api/views.py:3428 +#: api/views.py:3344 msgid "Job Host Summaries List" msgstr "" -#: awx/api/views.py:3470 +#: api/views.py:3386 msgid "Job Event Children List" msgstr "" -#: awx/api/views.py:3479 +#: api/views.py:3395 msgid "Job Event Hosts List" msgstr "" -#: awx/api/views.py:3488 +#: api/views.py:3404 msgid "Job Events List" msgstr "" -#: awx/api/views.py:3509 +#: api/views.py:3436 msgid "Job Plays List" msgstr "" -#: awx/api/views.py:3584 +#: api/views.py:3513 msgid "Job Play Tasks List" msgstr "" -#: awx/api/views.py:3599 +#: api/views.py:3529 msgid "Job not found." msgstr "" -#: awx/api/views.py:3603 +#: api/views.py:3533 msgid "'event_id' not provided." msgstr "" -#: awx/api/views.py:3607 +#: api/views.py:3537 msgid "Parent event not found." msgstr "" -#: awx/api/views.py:3879 +#: api/views.py:3809 msgid "Ad Hoc Command Events List" msgstr "" -#: awx/api/views.py:4043 +#: api/views.py:3963 #, python-format msgid "Error generating stdout download file: %s" msgstr "" -#: awx/api/views.py:4089 +#: api/views.py:4009 msgid "Delete not allowed while there are pending notifications" msgstr "" -#: awx/api/views.py:4096 +#: api/views.py:4016 msgid "NotificationTemplate Test" msgstr "" -#: awx/api/views.py:4231 +#: api/views.py:4131 msgid "User 'id' field is missing." msgstr "" -#: awx/api/views.py:4274 +#: api/views.py:4174 msgid "Team 'id' field is missing." msgstr "" -#: awx/conf/conf.py:20 +#: conf/conf.py:20 msgid "Bud Frogs" msgstr "" -#: awx/conf/conf.py:21 +#: conf/conf.py:21 msgid "Bunny" msgstr "" -#: awx/conf/conf.py:22 +#: conf/conf.py:22 msgid "Cheese" msgstr "" -#: awx/conf/conf.py:23 +#: conf/conf.py:23 msgid "Daemon" msgstr "" -#: awx/conf/conf.py:24 +#: conf/conf.py:24 msgid "Default Cow" msgstr "" -#: awx/conf/conf.py:25 +#: conf/conf.py:25 msgid "Dragon" msgstr "" -#: awx/conf/conf.py:26 +#: conf/conf.py:26 msgid "Elephant in Snake" msgstr "" -#: awx/conf/conf.py:27 +#: conf/conf.py:27 msgid "Elephant" msgstr "" -#: awx/conf/conf.py:28 +#: conf/conf.py:28 msgid "Eyes" msgstr "" -#: awx/conf/conf.py:29 +#: conf/conf.py:29 msgid "Hello Kitty" msgstr "" -#: awx/conf/conf.py:30 +#: conf/conf.py:30 msgid "Kitty" msgstr "" -#: awx/conf/conf.py:31 +#: conf/conf.py:31 msgid "Luke Koala" msgstr "" -#: awx/conf/conf.py:32 +#: conf/conf.py:32 msgid "Meow" msgstr "" -#: awx/conf/conf.py:33 +#: conf/conf.py:33 msgid "Milk" msgstr "" -#: awx/conf/conf.py:34 +#: conf/conf.py:34 msgid "Moofasa" msgstr "" -#: awx/conf/conf.py:35 +#: conf/conf.py:35 msgid "Moose" msgstr "" -#: awx/conf/conf.py:36 +#: conf/conf.py:36 msgid "Ren" msgstr "" -#: awx/conf/conf.py:37 +#: conf/conf.py:37 msgid "Sheep" msgstr "" -#: awx/conf/conf.py:38 +#: conf/conf.py:38 msgid "Small Cow" msgstr "" -#: awx/conf/conf.py:39 +#: conf/conf.py:39 msgid "Stegosaurus" msgstr "" -#: awx/conf/conf.py:40 +#: conf/conf.py:40 msgid "Stimpy" msgstr "" -#: awx/conf/conf.py:41 +#: conf/conf.py:41 msgid "Super Milker" msgstr "" -#: awx/conf/conf.py:42 +#: conf/conf.py:42 msgid "Three Eyes" msgstr "" -#: awx/conf/conf.py:43 +#: conf/conf.py:43 msgid "Turkey" msgstr "" -#: awx/conf/conf.py:44 +#: conf/conf.py:44 msgid "Turtle" msgstr "" -#: awx/conf/conf.py:45 +#: conf/conf.py:45 msgid "Tux" msgstr "" -#: awx/conf/conf.py:46 +#: conf/conf.py:46 msgid "Udder" msgstr "" -#: awx/conf/conf.py:47 +#: conf/conf.py:47 msgid "Vader Koala" msgstr "" -#: awx/conf/conf.py:48 +#: conf/conf.py:48 msgid "Vader" msgstr "" -#: awx/conf/conf.py:49 +#: conf/conf.py:49 msgid "WWW" msgstr "" -#: awx/conf/conf.py:52 +#: conf/conf.py:52 msgid "Cow Selection" msgstr "" -#: awx/conf/conf.py:53 +#: conf/conf.py:53 msgid "Select which cow to use with cowsay when running jobs." msgstr "" -#: awx/conf/conf.py:54 awx/conf/conf.py:75 +#: conf/conf.py:54 conf/conf.py:75 msgid "Cows" msgstr "" -#: awx/conf/conf.py:73 +#: conf/conf.py:73 msgid "Example Read-Only Setting" msgstr "" -#: awx/conf/conf.py:74 +#: conf/conf.py:74 msgid "Example setting that cannot be changed." msgstr "" -#: awx/conf/conf.py:90 +#: conf/conf.py:93 msgid "Example Setting" msgstr "" -#: awx/conf/conf.py:91 +#: conf/conf.py:94 msgid "Example setting which can be different for each user." msgstr "" -#: awx/conf/conf.py:92 awx/conf/registry.py:67 awx/conf/views.py:46 +#: conf/conf.py:95 conf/registry.py:67 conf/views.py:46 msgid "User" msgstr "" -#: awx/conf/fields.py:38 +#: conf/fields.py:38 msgid "Enter a valid URL" msgstr "" -#: awx/conf/license.py:23 +#: conf/license.py:19 msgid "Your Tower license does not allow that." msgstr "" -#: awx/conf/management/commands/migrate_to_database_settings.py:41 +#: conf/management/commands/migrate_to_database_settings.py:41 msgid "Only show which settings would be commented/migrated." msgstr "" -#: awx/conf/management/commands/migrate_to_database_settings.py:48 +#: conf/management/commands/migrate_to_database_settings.py:48 msgid "Skip over settings that would raise an error when commenting/migrating." msgstr "" -#: awx/conf/management/commands/migrate_to_database_settings.py:55 +#: conf/management/commands/migrate_to_database_settings.py:55 msgid "Skip commenting out settings in files." msgstr "" -#: awx/conf/management/commands/migrate_to_database_settings.py:61 +#: conf/management/commands/migrate_to_database_settings.py:61 msgid "Backup existing settings files with this suffix." msgstr "" -#: awx/conf/registry.py:55 +#: conf/registry.py:55 msgid "All" msgstr "" -#: awx/conf/registry.py:56 +#: conf/registry.py:56 msgid "Changed" msgstr "" -#: awx/conf/registry.py:68 +#: conf/registry.py:68 msgid "User-Defaults" msgstr "" -#: awx/conf/views.py:38 +#: conf/views.py:38 msgid "Setting Categories" msgstr "" -#: awx/conf/views.py:61 +#: conf/views.py:61 msgid "Setting Detail" msgstr "" -#: awx/main/access.py:255 +#: main/access.py:255 #, python-format msgid "Bad data found in related field %s." msgstr "" -#: awx/main/access.py:296 +#: main/access.py:296 msgid "License is missing." msgstr "" -#: awx/main/access.py:298 +#: main/access.py:298 msgid "License has expired." msgstr "" -#: awx/main/access.py:303 +#: main/access.py:303 #, python-format msgid "License count of %s instances has been reached." msgstr "" -#: awx/main/access.py:305 +#: main/access.py:305 #, python-format msgid "License count of %s instances has been exceeded." msgstr "" -#: awx/main/access.py:307 +#: main/access.py:307 msgid "Host count exceeds available instances." msgstr "" -#: awx/main/access.py:311 +#: main/access.py:311 #, python-format msgid "Feature %s is not enabled in the active license." msgstr "" -#: awx/main/access.py:313 +#: main/access.py:313 msgid "Features not found in active license." msgstr "" -#: awx/main/access.py:507 awx/main/access.py:574 awx/main/access.py:694 -#: awx/main/access.py:965 awx/main/access.py:1206 awx/main/access.py:1594 +#: main/access.py:507 main/access.py:574 main/access.py:694 main/access.py:957 +#: main/access.py:1198 main/access.py:1587 msgid "Resource is being used by running jobs" msgstr "" -#: awx/main/access.py:618 +#: main/access.py:618 msgid "Unable to change inventory on a host." msgstr "" -#: awx/main/access.py:630 awx/main/access.py:675 +#: main/access.py:630 main/access.py:675 msgid "Cannot associate two items from different inventories." msgstr "" -#: awx/main/access.py:663 +#: main/access.py:663 msgid "Unable to change inventory on a group." msgstr "" -#: awx/main/access.py:885 +#: main/access.py:877 msgid "Unable to change organization on a team." msgstr "" -#: awx/main/access.py:898 +#: main/access.py:890 msgid "The {} role cannot be assigned to a team" msgstr "" -#: awx/main/access.py:900 +#: main/access.py:892 msgid "The admin_role for a User cannot be assigned to a team" msgstr "" -#: awx/main/apps.py:9 +#: main/apps.py:9 msgid "Main" msgstr "" -#: awx/main/conf.py:17 +#: main/conf.py:17 msgid "Enable Activity Stream" msgstr "" -#: awx/main/conf.py:18 +#: main/conf.py:18 msgid "Enable capturing activity for the Tower activity stream." msgstr "" -#: awx/main/conf.py:19 awx/main/conf.py:29 awx/main/conf.py:39 -#: awx/main/conf.py:48 awx/main/conf.py:60 awx/main/conf.py:78 -#: awx/main/conf.py:103 +#: main/conf.py:19 main/conf.py:29 main/conf.py:39 main/conf.py:48 +#: main/conf.py:60 main/conf.py:78 main/conf.py:103 msgid "System" msgstr "" -#: awx/main/conf.py:27 +#: main/conf.py:27 msgid "Enable Activity Stream for Inventory Sync" msgstr "" -#: awx/main/conf.py:28 +#: main/conf.py:28 msgid "" "Enable capturing activity for the Tower activity stream when running " "inventory sync." msgstr "" -#: awx/main/conf.py:37 +#: main/conf.py:37 msgid "All Users Visible to Organization Admins" msgstr "" -#: awx/main/conf.py:38 +#: main/conf.py:38 msgid "" "Controls whether any Organization Admin can view all users, even those not " "associated with their Organization." msgstr "" -#: awx/main/conf.py:46 +#: main/conf.py:46 msgid "Enable Tower Administrator Alerts" msgstr "" -#: awx/main/conf.py:47 +#: main/conf.py:47 msgid "" "Allow Tower to email Admin users for system events that may require " "attention." msgstr "" -#: awx/main/conf.py:57 +#: main/conf.py:57 msgid "Base URL of the Tower host" msgstr "" -#: awx/main/conf.py:58 +#: main/conf.py:58 msgid "" "This setting is used by services like notifications to render a valid url to " "the Tower host." msgstr "" -#: awx/main/conf.py:67 +#: main/conf.py:67 msgid "Remote Host Headers" msgstr "" -#: awx/main/conf.py:68 +#: main/conf.py:68 msgid "" "HTTP headers and meta keys to search to determine remote host name or IP. " "Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if " @@ -1056,1459 +1064,1615 @@ msgid "" "REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', 'REMOTE_HOST']" msgstr "" -#: awx/main/conf.py:99 +#: main/conf.py:99 msgid "Tower License" msgstr "" -#: awx/main/conf.py:100 +#: main/conf.py:100 msgid "" "The license controls which features and functionality are enabled in Tower. " "Use /api/v1/config/ to update or change the license." msgstr "" -#: awx/main/conf.py:110 +#: main/conf.py:110 msgid "Ansible Modules Allowed for Ad Hoc Jobs" msgstr "" -#: awx/main/conf.py:111 +#: main/conf.py:111 msgid "List of modules allowed to be used by ad-hoc jobs." msgstr "" -#: awx/main/conf.py:112 awx/main/conf.py:121 awx/main/conf.py:130 -#: awx/main/conf.py:139 awx/main/conf.py:148 awx/main/conf.py:158 -#: awx/main/conf.py:168 awx/main/conf.py:178 awx/main/conf.py:187 -#: awx/main/conf.py:199 awx/main/conf.py:211 awx/main/conf.py:223 +#: main/conf.py:112 main/conf.py:121 main/conf.py:130 main/conf.py:139 +#: main/conf.py:148 main/conf.py:158 main/conf.py:168 main/conf.py:178 +#: main/conf.py:187 main/conf.py:199 main/conf.py:211 main/conf.py:223 msgid "Jobs" msgstr "" -#: awx/main/conf.py:119 +#: main/conf.py:119 msgid "Enable job isolation" msgstr "" -#: awx/main/conf.py:120 +#: main/conf.py:120 msgid "" "Isolates an Ansible job from protected parts of the Tower system to prevent " "exposing sensitive information." msgstr "" -#: awx/main/conf.py:128 +#: main/conf.py:128 msgid "Job isolation execution path" msgstr "" -#: awx/main/conf.py:129 +#: main/conf.py:129 msgid "" "Create temporary working directories for isolated jobs in this location." msgstr "" -#: awx/main/conf.py:137 +#: main/conf.py:137 msgid "Paths to hide from isolated jobs" msgstr "" -#: awx/main/conf.py:138 +#: main/conf.py:138 msgid "Additional paths to hide from isolated processes." msgstr "" -#: awx/main/conf.py:146 +#: main/conf.py:146 msgid "Paths to expose to isolated jobs" msgstr "" -#: awx/main/conf.py:147 +#: main/conf.py:147 msgid "" "Whitelist of paths that would otherwise be hidden to expose to isolated jobs." msgstr "" -#: awx/main/conf.py:156 +#: main/conf.py:156 msgid "Standard Output Maximum Display Size" msgstr "" -#: awx/main/conf.py:157 +#: main/conf.py:157 msgid "" "Maximum Size of Standard Output in bytes to display before requiring the " "output be downloaded." msgstr "" -#: awx/main/conf.py:166 +#: main/conf.py:166 msgid "Job Event Standard Output Maximum Display Size" msgstr "" -#: awx/main/conf.py:167 +#: main/conf.py:167 msgid "" "Maximum Size of Standard Output in bytes to display for a single job or ad " "hoc command event. `stdout` will end with `…` when truncated." msgstr "" -#: awx/main/conf.py:176 +#: main/conf.py:176 msgid "Maximum Scheduled Jobs" msgstr "" -#: awx/main/conf.py:177 +#: main/conf.py:177 msgid "" "Maximum number of the same job template that can be waiting to run when " "launching from a schedule before no more are created." msgstr "" -#: awx/main/conf.py:185 +#: main/conf.py:185 msgid "Ansible Callback Plugins" msgstr "" -#: awx/main/conf.py:186 +#: main/conf.py:186 msgid "" "List of paths to search for extra callback plugins to be used when running " "jobs." msgstr "" -#: awx/main/conf.py:196 +#: main/conf.py:196 msgid "Default Job Timeout" msgstr "" -#: awx/main/conf.py:197 +#: main/conf.py:197 msgid "" "Maximum time to allow jobs to run. Use value of 0 to indicate that no " "timeout should be imposed. A timeout set on an individual job template will " "override this." msgstr "" -#: awx/main/conf.py:208 +#: main/conf.py:208 msgid "Default Inventory Update Timeout" msgstr "" -#: awx/main/conf.py:209 +#: main/conf.py:209 msgid "" "Maximum time to allow inventory updates to run. Use value of 0 to indicate " "that no timeout should be imposed. A timeout set on an individual inventory " "source will override this." msgstr "" -#: awx/main/conf.py:220 +#: main/conf.py:220 msgid "Default Project Update Timeout" msgstr "" -#: awx/main/conf.py:221 +#: main/conf.py:221 msgid "" "Maximum time to allow project updates to run. Use value of 0 to indicate " "that no timeout should be imposed. A timeout set on an individual project " "will override this." msgstr "" -#: awx/main/models/activity_stream.py:22 +#: main/conf.py:231 +msgid "Logging Aggregator Receiving Host" +msgstr "" + +#: main/conf.py:232 +msgid "External host maintain a log collector to send logs to" +msgstr "" + +#: main/conf.py:233 main/conf.py:242 main/conf.py:252 main/conf.py:261 +#: main/conf.py:271 main/conf.py:286 main/conf.py:297 main/conf.py:306 +msgid "Logging" +msgstr "" + +#: main/conf.py:240 +msgid "Logging Aggregator Receiving Port" +msgstr "" + +#: main/conf.py:241 +msgid "Port that the log collector is listening on" +msgstr "" + +#: main/conf.py:250 +msgid "Logging Aggregator Type: Logstash, Loggly, Datadog, etc" +msgstr "" + +#: main/conf.py:251 +msgid "The type of log aggregator service to format messages for" +msgstr "" + +#: main/conf.py:259 +msgid "Logging Aggregator Username to Authenticate With" +msgstr "" + +#: main/conf.py:260 +msgid "Username for Logstash or others (basic auth)" +msgstr "" + +#: main/conf.py:269 +msgid "Logging Aggregator Password to Authenticate With" +msgstr "" + +#: main/conf.py:270 +msgid "Password for Logstash or others (basic auth)" +msgstr "" + +#: main/conf.py:278 +msgid "Loggers to send data to the log aggregator from" +msgstr "" + +#: main/conf.py:279 +msgid "" +"List of loggers that will send HTTP logs to the collector, these can include " +"any or all of: \n" +"activity_stream - logs duplicate to records entered in activity stream\n" +"job_events - callback data from Ansible job events\n" +"system_tracking - data generated from scan jobs\n" +"Sending generic Tower logs must be configured through local_settings." +"pyinstead of this mechanism." +msgstr "" + +#: main/conf.py:293 +msgid "" +"Flag denoting to send individual messages for each fact in system tracking" +msgstr "" + +#: main/conf.py:294 +msgid "" +"If not set, the data from system tracking will be sent inside of a single " +"dictionary, but if set, separate requests will be sent for each package, " +"service, etc. that is found in the scan." +msgstr "" + +#: main/conf.py:304 +msgid "Flag denoting whether to use the external logger system" +msgstr "" + +#: main/conf.py:305 +msgid "" +"If not set, only normal settings data will be used to configure loggers." +msgstr "" + +#: main/models/activity_stream.py:22 msgid "Entity Created" msgstr "" -#: awx/main/models/activity_stream.py:23 +#: main/models/activity_stream.py:23 msgid "Entity Updated" msgstr "" -#: awx/main/models/activity_stream.py:24 +#: main/models/activity_stream.py:24 msgid "Entity Deleted" msgstr "" -#: awx/main/models/activity_stream.py:25 +#: main/models/activity_stream.py:25 msgid "Entity Associated with another Entity" msgstr "" -#: awx/main/models/activity_stream.py:26 +#: main/models/activity_stream.py:26 msgid "Entity was Disassociated with another Entity" msgstr "" -#: awx/main/models/ad_hoc_commands.py:96 +#: main/models/ad_hoc_commands.py:96 msgid "No valid inventory." msgstr "" -#: awx/main/models/ad_hoc_commands.py:103 awx/main/models/jobs.py:162 +#: main/models/ad_hoc_commands.py:103 main/models/jobs.py:163 msgid "You must provide a machine / SSH credential." msgstr "" -#: awx/main/models/ad_hoc_commands.py:114 -#: awx/main/models/ad_hoc_commands.py:122 +#: main/models/ad_hoc_commands.py:114 main/models/ad_hoc_commands.py:122 msgid "Invalid type for ad hoc command" msgstr "" -#: awx/main/models/ad_hoc_commands.py:117 +#: main/models/ad_hoc_commands.py:117 msgid "Unsupported module for ad hoc commands." msgstr "" -#: awx/main/models/ad_hoc_commands.py:125 +#: main/models/ad_hoc_commands.py:125 #, python-format msgid "No argument passed to %s module." msgstr "" -#: awx/main/models/ad_hoc_commands.py:220 awx/main/models/jobs.py:766 +#: main/models/ad_hoc_commands.py:220 main/models/jobs.py:767 msgid "Host Failed" msgstr "" -#: awx/main/models/ad_hoc_commands.py:221 awx/main/models/jobs.py:767 +#: main/models/ad_hoc_commands.py:221 main/models/jobs.py:768 msgid "Host OK" msgstr "" -#: awx/main/models/ad_hoc_commands.py:222 awx/main/models/jobs.py:770 +#: main/models/ad_hoc_commands.py:222 main/models/jobs.py:771 msgid "Host Unreachable" msgstr "" -#: awx/main/models/ad_hoc_commands.py:227 awx/main/models/jobs.py:769 +#: main/models/ad_hoc_commands.py:227 main/models/jobs.py:770 msgid "Host Skipped" msgstr "" -#: awx/main/models/ad_hoc_commands.py:237 awx/main/models/jobs.py:797 +#: main/models/ad_hoc_commands.py:237 main/models/jobs.py:798 msgid "Debug" msgstr "" -#: awx/main/models/ad_hoc_commands.py:238 awx/main/models/jobs.py:798 +#: main/models/ad_hoc_commands.py:238 main/models/jobs.py:799 msgid "Verbose" msgstr "" -#: awx/main/models/ad_hoc_commands.py:239 awx/main/models/jobs.py:799 +#: main/models/ad_hoc_commands.py:239 main/models/jobs.py:800 msgid "Deprecated" msgstr "" -#: awx/main/models/ad_hoc_commands.py:240 awx/main/models/jobs.py:800 +#: main/models/ad_hoc_commands.py:240 main/models/jobs.py:801 msgid "Warning" msgstr "" -#: awx/main/models/ad_hoc_commands.py:241 awx/main/models/jobs.py:801 +#: main/models/ad_hoc_commands.py:241 main/models/jobs.py:802 msgid "System Warning" msgstr "" -#: awx/main/models/ad_hoc_commands.py:242 awx/main/models/jobs.py:802 -#: awx/main/models/unified_jobs.py:62 +#: main/models/ad_hoc_commands.py:242 main/models/jobs.py:803 +#: main/models/unified_jobs.py:62 msgid "Error" msgstr "" -#: awx/main/models/base.py:45 awx/main/models/base.py:51 -#: awx/main/models/base.py:56 +#: main/models/base.py:45 main/models/base.py:51 main/models/base.py:56 msgid "Run" msgstr "" -#: awx/main/models/base.py:46 awx/main/models/base.py:52 -#: awx/main/models/base.py:57 +#: main/models/base.py:46 main/models/base.py:52 main/models/base.py:57 msgid "Check" msgstr "" -#: awx/main/models/base.py:47 +#: main/models/base.py:47 msgid "Scan" msgstr "" -#: awx/main/models/base.py:61 +#: main/models/base.py:61 msgid "Read Inventory" msgstr "" -#: awx/main/models/base.py:62 +#: main/models/base.py:62 msgid "Edit Inventory" msgstr "" -#: awx/main/models/base.py:63 +#: main/models/base.py:63 msgid "Administrate Inventory" msgstr "" -#: awx/main/models/base.py:64 +#: main/models/base.py:64 msgid "Deploy To Inventory" msgstr "" -#: awx/main/models/base.py:65 +#: main/models/base.py:65 msgid "Deploy To Inventory (Dry Run)" msgstr "" -#: awx/main/models/base.py:66 +#: main/models/base.py:66 msgid "Scan an Inventory" msgstr "" -#: awx/main/models/base.py:67 +#: main/models/base.py:67 msgid "Create a Job Template" msgstr "" -#: awx/main/models/credential.py:33 +#: main/models/credential.py:33 msgid "Machine" msgstr "" -#: awx/main/models/credential.py:34 +#: main/models/credential.py:34 msgid "Network" msgstr "" -#: awx/main/models/credential.py:35 +#: main/models/credential.py:35 msgid "Source Control" msgstr "" -#: awx/main/models/credential.py:36 +#: main/models/credential.py:36 msgid "Amazon Web Services" msgstr "" -#: awx/main/models/credential.py:37 +#: main/models/credential.py:37 msgid "Rackspace" msgstr "" -#: awx/main/models/credential.py:38 awx/main/models/inventory.py:712 +#: main/models/credential.py:38 main/models/inventory.py:713 msgid "VMware vCenter" msgstr "" -#: awx/main/models/credential.py:39 awx/main/models/inventory.py:713 +#: main/models/credential.py:39 main/models/inventory.py:714 msgid "Red Hat Satellite 6" msgstr "" -#: awx/main/models/credential.py:40 awx/main/models/inventory.py:714 +#: main/models/credential.py:40 main/models/inventory.py:715 msgid "Red Hat CloudForms" msgstr "" -#: awx/main/models/credential.py:41 awx/main/models/inventory.py:709 +#: main/models/credential.py:41 main/models/inventory.py:710 msgid "Google Compute Engine" msgstr "" -#: awx/main/models/credential.py:42 awx/main/models/inventory.py:710 +#: main/models/credential.py:42 main/models/inventory.py:711 msgid "Microsoft Azure Classic (deprecated)" msgstr "" -#: awx/main/models/credential.py:43 awx/main/models/inventory.py:711 +#: main/models/credential.py:43 main/models/inventory.py:712 msgid "Microsoft Azure Resource Manager" msgstr "" -#: awx/main/models/credential.py:44 awx/main/models/inventory.py:715 +#: main/models/credential.py:44 main/models/inventory.py:716 msgid "OpenStack" msgstr "" -#: awx/main/models/credential.py:48 +#: main/models/credential.py:48 msgid "None" msgstr "" -#: awx/main/models/credential.py:49 +#: main/models/credential.py:49 msgid "Sudo" msgstr "" -#: awx/main/models/credential.py:50 +#: main/models/credential.py:50 msgid "Su" msgstr "" -#: awx/main/models/credential.py:51 +#: main/models/credential.py:51 msgid "Pbrun" msgstr "" -#: awx/main/models/credential.py:52 +#: main/models/credential.py:52 msgid "Pfexec" msgstr "" -#: awx/main/models/credential.py:101 +#: main/models/credential.py:101 msgid "Host" msgstr "" -#: awx/main/models/credential.py:102 +#: main/models/credential.py:102 msgid "The hostname or IP address to use." msgstr "" -#: awx/main/models/credential.py:108 +#: main/models/credential.py:108 msgid "Username" msgstr "" -#: awx/main/models/credential.py:109 +#: main/models/credential.py:109 msgid "Username for this credential." msgstr "" -#: awx/main/models/credential.py:115 +#: main/models/credential.py:115 msgid "Password" msgstr "" -#: awx/main/models/credential.py:116 +#: main/models/credential.py:116 msgid "" "Password for this credential (or \"ASK\" to prompt the user for machine " "credentials)." msgstr "" -#: awx/main/models/credential.py:123 +#: main/models/credential.py:123 msgid "Security Token" msgstr "" -#: awx/main/models/credential.py:124 +#: main/models/credential.py:124 msgid "Security Token for this credential" msgstr "" -#: awx/main/models/credential.py:130 +#: main/models/credential.py:130 msgid "Project" msgstr "" -#: awx/main/models/credential.py:131 +#: main/models/credential.py:131 msgid "The identifier for the project." msgstr "" -#: awx/main/models/credential.py:137 +#: main/models/credential.py:137 msgid "Domain" msgstr "" -#: awx/main/models/credential.py:138 +#: main/models/credential.py:138 msgid "The identifier for the domain." msgstr "" -#: awx/main/models/credential.py:143 +#: main/models/credential.py:143 msgid "SSH private key" msgstr "" -#: awx/main/models/credential.py:144 +#: main/models/credential.py:144 msgid "RSA or DSA private key to be used instead of password." msgstr "" -#: awx/main/models/credential.py:150 +#: main/models/credential.py:150 msgid "SSH key unlock" msgstr "" -#: awx/main/models/credential.py:151 +#: main/models/credential.py:151 msgid "" "Passphrase to unlock SSH private key if encrypted (or \"ASK\" to prompt the " "user for machine credentials)." msgstr "" -#: awx/main/models/credential.py:159 +#: main/models/credential.py:159 msgid "Privilege escalation method." msgstr "" -#: awx/main/models/credential.py:165 +#: main/models/credential.py:165 msgid "Privilege escalation username." msgstr "" -#: awx/main/models/credential.py:171 +#: main/models/credential.py:171 msgid "Password for privilege escalation method." msgstr "" -#: awx/main/models/credential.py:177 +#: main/models/credential.py:177 msgid "Vault password (or \"ASK\" to prompt the user)." msgstr "" -#: awx/main/models/credential.py:181 +#: main/models/credential.py:181 msgid "Whether to use the authorize mechanism." msgstr "" -#: awx/main/models/credential.py:187 +#: main/models/credential.py:187 msgid "Password used by the authorize mechanism." msgstr "" -#: awx/main/models/credential.py:193 +#: main/models/credential.py:193 msgid "Client Id or Application Id for the credential" msgstr "" -#: awx/main/models/credential.py:199 +#: main/models/credential.py:199 msgid "Secret Token for this credential" msgstr "" -#: awx/main/models/credential.py:205 +#: main/models/credential.py:205 msgid "Subscription identifier for this credential" msgstr "" -#: awx/main/models/credential.py:211 +#: main/models/credential.py:211 msgid "Tenant identifier for this credential" msgstr "" -#: awx/main/models/credential.py:281 +#: main/models/credential.py:281 msgid "Host required for VMware credential." msgstr "" -#: awx/main/models/credential.py:283 +#: main/models/credential.py:283 msgid "Host required for OpenStack credential." msgstr "" -#: awx/main/models/credential.py:292 +#: main/models/credential.py:292 msgid "Access key required for AWS credential." msgstr "" -#: awx/main/models/credential.py:294 +#: main/models/credential.py:294 msgid "Username required for Rackspace credential." msgstr "" -#: awx/main/models/credential.py:297 +#: main/models/credential.py:297 msgid "Username required for VMware credential." msgstr "" -#: awx/main/models/credential.py:299 +#: main/models/credential.py:299 msgid "Username required for OpenStack credential." msgstr "" -#: awx/main/models/credential.py:305 +#: main/models/credential.py:305 msgid "Secret key required for AWS credential." msgstr "" -#: awx/main/models/credential.py:307 +#: main/models/credential.py:307 msgid "API key required for Rackspace credential." msgstr "" -#: awx/main/models/credential.py:309 +#: main/models/credential.py:309 msgid "Password required for VMware credential." msgstr "" -#: awx/main/models/credential.py:311 +#: main/models/credential.py:311 msgid "Password or API key required for OpenStack credential." msgstr "" -#: awx/main/models/credential.py:317 +#: main/models/credential.py:317 msgid "Project name required for OpenStack credential." msgstr "" -#: awx/main/models/credential.py:344 +#: main/models/credential.py:344 msgid "SSH key unlock must be set when SSH key is encrypted." msgstr "" -#: awx/main/models/credential.py:350 +#: main/models/credential.py:350 msgid "Credential cannot be assigned to both a user and team." msgstr "" -#: awx/main/models/fact.py:21 +#: main/models/fact.py:21 msgid "Host for the facts that the fact scan captured." msgstr "" -#: awx/main/models/fact.py:26 +#: main/models/fact.py:26 msgid "Date and time of the corresponding fact scan gathering time." msgstr "" -#: awx/main/models/fact.py:29 +#: main/models/fact.py:29 msgid "" "Arbitrary JSON structure of module facts captured at timestamp for a single " "host." msgstr "" -#: awx/main/models/inventory.py:45 +#: main/models/inventory.py:45 msgid "inventories" msgstr "" -#: awx/main/models/inventory.py:52 +#: main/models/inventory.py:52 msgid "Organization containing this inventory." msgstr "" -#: awx/main/models/inventory.py:58 +#: main/models/inventory.py:58 msgid "Inventory variables in JSON or YAML format." msgstr "" -#: awx/main/models/inventory.py:63 +#: main/models/inventory.py:63 msgid "Flag indicating whether any hosts in this inventory have failed." msgstr "" -#: awx/main/models/inventory.py:68 +#: main/models/inventory.py:68 msgid "Total number of hosts in this inventory." msgstr "" -#: awx/main/models/inventory.py:73 +#: main/models/inventory.py:73 msgid "Number of hosts in this inventory with active failures." msgstr "" -#: awx/main/models/inventory.py:78 +#: main/models/inventory.py:78 msgid "Total number of groups in this inventory." msgstr "" -#: awx/main/models/inventory.py:83 +#: main/models/inventory.py:83 msgid "Number of groups in this inventory with active failures." msgstr "" -#: awx/main/models/inventory.py:88 +#: main/models/inventory.py:88 msgid "" "Flag indicating whether this inventory has any external inventory sources." msgstr "" -#: awx/main/models/inventory.py:93 +#: main/models/inventory.py:93 msgid "" "Total number of external inventory sources configured within this inventory." msgstr "" -#: awx/main/models/inventory.py:98 +#: main/models/inventory.py:98 msgid "Number of external inventory sources in this inventory with failures." msgstr "" -#: awx/main/models/inventory.py:339 +#: main/models/inventory.py:339 msgid "Is this host online and available for running jobs?" msgstr "" -#: awx/main/models/inventory.py:349 +#: main/models/inventory.py:345 +msgid "" +"The value used by the remote inventory source to uniquely identify the host" +msgstr "" + +#: main/models/inventory.py:350 msgid "Host variables in JSON or YAML format." msgstr "" -#: awx/main/models/inventory.py:371 +#: main/models/inventory.py:372 msgid "Flag indicating whether the last job failed for this host." msgstr "" -#: awx/main/models/inventory.py:376 +#: main/models/inventory.py:377 msgid "" "Flag indicating whether this host was created/updated from any external " "inventory sources." msgstr "" -#: awx/main/models/inventory.py:382 +#: main/models/inventory.py:383 msgid "Inventory source(s) that created or modified this host." msgstr "" -#: awx/main/models/inventory.py:473 +#: main/models/inventory.py:474 msgid "Group variables in JSON or YAML format." msgstr "" -#: awx/main/models/inventory.py:479 +#: main/models/inventory.py:480 msgid "Hosts associated directly with this group." msgstr "" -#: awx/main/models/inventory.py:484 +#: main/models/inventory.py:485 msgid "Total number of hosts directly or indirectly in this group." msgstr "" -#: awx/main/models/inventory.py:489 +#: main/models/inventory.py:490 msgid "Flag indicating whether this group has any hosts with active failures." msgstr "" -#: awx/main/models/inventory.py:494 +#: main/models/inventory.py:495 msgid "Number of hosts in this group with active failures." msgstr "" -#: awx/main/models/inventory.py:499 +#: main/models/inventory.py:500 msgid "Total number of child groups contained within this group." msgstr "" -#: awx/main/models/inventory.py:504 +#: main/models/inventory.py:505 msgid "Number of child groups within this group that have active failures." msgstr "" -#: awx/main/models/inventory.py:509 +#: main/models/inventory.py:510 msgid "" "Flag indicating whether this group was created/updated from any external " "inventory sources." msgstr "" -#: awx/main/models/inventory.py:515 +#: main/models/inventory.py:516 msgid "Inventory source(s) that created or modified this group." msgstr "" -#: awx/main/models/inventory.py:705 awx/main/models/projects.py:42 -#: awx/main/models/unified_jobs.py:383 +#: main/models/inventory.py:706 main/models/projects.py:42 +#: main/models/unified_jobs.py:386 msgid "Manual" msgstr "" -#: awx/main/models/inventory.py:706 +#: main/models/inventory.py:707 msgid "Local File, Directory or Script" msgstr "" -#: awx/main/models/inventory.py:707 +#: main/models/inventory.py:708 msgid "Rackspace Cloud Servers" msgstr "" -#: awx/main/models/inventory.py:708 +#: main/models/inventory.py:709 msgid "Amazon EC2" msgstr "" -#: awx/main/models/inventory.py:716 +#: main/models/inventory.py:717 msgid "Custom Script" msgstr "" -#: awx/main/models/inventory.py:827 +#: main/models/inventory.py:828 msgid "Inventory source variables in YAML or JSON format." msgstr "" -#: awx/main/models/inventory.py:846 +#: main/models/inventory.py:847 msgid "" "Comma-separated list of filter expressions (EC2 only). Hosts are imported " "when ANY of the filters match." msgstr "" -#: awx/main/models/inventory.py:852 +#: main/models/inventory.py:853 msgid "Limit groups automatically created from inventory source (EC2 only)." msgstr "" -#: awx/main/models/inventory.py:856 +#: main/models/inventory.py:857 msgid "Overwrite local groups and hosts from remote inventory source." msgstr "" -#: awx/main/models/inventory.py:860 +#: main/models/inventory.py:861 msgid "Overwrite local variables from remote inventory source." msgstr "" -#: awx/main/models/inventory.py:892 +#: main/models/inventory.py:893 msgid "Availability Zone" msgstr "" -#: awx/main/models/inventory.py:893 +#: main/models/inventory.py:894 msgid "Image ID" msgstr "" -#: awx/main/models/inventory.py:894 +#: main/models/inventory.py:895 msgid "Instance ID" msgstr "" -#: awx/main/models/inventory.py:895 +#: main/models/inventory.py:896 msgid "Instance Type" msgstr "" -#: awx/main/models/inventory.py:896 +#: main/models/inventory.py:897 msgid "Key Name" msgstr "" -#: awx/main/models/inventory.py:897 +#: main/models/inventory.py:898 msgid "Region" msgstr "" -#: awx/main/models/inventory.py:898 +#: main/models/inventory.py:899 msgid "Security Group" msgstr "" -#: awx/main/models/inventory.py:899 +#: main/models/inventory.py:900 msgid "Tags" msgstr "" -#: awx/main/models/inventory.py:900 +#: main/models/inventory.py:901 msgid "VPC ID" msgstr "" -#: awx/main/models/inventory.py:901 +#: main/models/inventory.py:902 msgid "Tag None" msgstr "" -#: awx/main/models/inventory.py:972 +#: main/models/inventory.py:973 #, python-format msgid "" "Cloud-based inventory sources (such as %s) require credentials for the " "matching cloud service." msgstr "" -#: awx/main/models/inventory.py:979 +#: main/models/inventory.py:980 msgid "Credential is required for a cloud source." msgstr "" -#: awx/main/models/inventory.py:1004 +#: main/models/inventory.py:1005 #, python-format msgid "Invalid %(source)s region%(plural)s: %(region)s" msgstr "" -#: awx/main/models/inventory.py:1030 +#: main/models/inventory.py:1031 #, python-format msgid "Invalid filter expression%(plural)s: %(filter)s" msgstr "" -#: awx/main/models/inventory.py:1049 +#: main/models/inventory.py:1050 #, python-format msgid "Invalid group by choice%(plural)s: %(choice)s" msgstr "" -#: awx/main/models/inventory.py:1197 +#: main/models/inventory.py:1198 #, python-format msgid "" "Unable to configure this item for cloud sync. It is already managed by %s." msgstr "" -#: awx/main/models/inventory.py:1292 +#: main/models/inventory.py:1293 msgid "Inventory script contents" msgstr "" -#: awx/main/models/inventory.py:1297 +#: main/models/inventory.py:1298 msgid "Organization owning this inventory script" msgstr "" -#: awx/main/models/jobs.py:170 +#: main/models/jobs.py:171 msgid "You must provide a network credential." msgstr "" -#: awx/main/models/jobs.py:178 +#: main/models/jobs.py:179 msgid "" "Must provide a credential for a cloud provider, such as Amazon Web Services " "or Rackspace." msgstr "" -#: awx/main/models/jobs.py:270 +#: main/models/jobs.py:271 msgid "Job Template must provide 'inventory' or allow prompting for it." msgstr "" -#: awx/main/models/jobs.py:274 +#: main/models/jobs.py:275 msgid "Job Template must provide 'credential' or allow prompting for it." msgstr "" -#: awx/main/models/jobs.py:363 +#: main/models/jobs.py:364 msgid "Cannot override job_type to or from a scan job." msgstr "" -#: awx/main/models/jobs.py:366 +#: main/models/jobs.py:367 msgid "Inventory cannot be changed at runtime for scan jobs." msgstr "" -#: awx/main/models/jobs.py:432 awx/main/models/projects.py:235 +#: main/models/jobs.py:433 main/models/projects.py:243 msgid "SCM Revision" msgstr "" -#: awx/main/models/jobs.py:433 +#: main/models/jobs.py:434 msgid "The SCM Revision from the Project used for this job, if available" msgstr "" -#: awx/main/models/jobs.py:441 +#: main/models/jobs.py:442 msgid "" "The SCM Refresh task used to make sure the playbooks were available for the " "job run" msgstr "" -#: awx/main/models/jobs.py:665 +#: main/models/jobs.py:666 msgid "job host summaries" msgstr "" -#: awx/main/models/jobs.py:768 +#: main/models/jobs.py:769 msgid "Host Failure" msgstr "" -#: awx/main/models/jobs.py:771 awx/main/models/jobs.py:785 +#: main/models/jobs.py:772 main/models/jobs.py:786 msgid "No Hosts Remaining" msgstr "" -#: awx/main/models/jobs.py:772 +#: main/models/jobs.py:773 msgid "Host Polling" msgstr "" -#: awx/main/models/jobs.py:773 +#: main/models/jobs.py:774 msgid "Host Async OK" msgstr "" -#: awx/main/models/jobs.py:774 +#: main/models/jobs.py:775 msgid "Host Async Failure" msgstr "" -#: awx/main/models/jobs.py:775 +#: main/models/jobs.py:776 msgid "Item OK" msgstr "" -#: awx/main/models/jobs.py:776 +#: main/models/jobs.py:777 msgid "Item Failed" msgstr "" -#: awx/main/models/jobs.py:777 +#: main/models/jobs.py:778 msgid "Item Skipped" msgstr "" -#: awx/main/models/jobs.py:778 +#: main/models/jobs.py:779 msgid "Host Retry" msgstr "" -#: awx/main/models/jobs.py:780 +#: main/models/jobs.py:781 msgid "File Difference" msgstr "" -#: awx/main/models/jobs.py:781 +#: main/models/jobs.py:782 msgid "Playbook Started" msgstr "" -#: awx/main/models/jobs.py:782 +#: main/models/jobs.py:783 msgid "Running Handlers" msgstr "" -#: awx/main/models/jobs.py:783 +#: main/models/jobs.py:784 msgid "Including File" msgstr "" -#: awx/main/models/jobs.py:784 +#: main/models/jobs.py:785 msgid "No Hosts Matched" msgstr "" -#: awx/main/models/jobs.py:786 +#: main/models/jobs.py:787 msgid "Task Started" msgstr "" -#: awx/main/models/jobs.py:788 +#: main/models/jobs.py:789 msgid "Variables Prompted" msgstr "" -#: awx/main/models/jobs.py:789 +#: main/models/jobs.py:790 msgid "Gathering Facts" msgstr "" -#: awx/main/models/jobs.py:790 +#: main/models/jobs.py:791 msgid "internal: on Import for Host" msgstr "" -#: awx/main/models/jobs.py:791 +#: main/models/jobs.py:792 msgid "internal: on Not Import for Host" msgstr "" -#: awx/main/models/jobs.py:792 +#: main/models/jobs.py:793 msgid "Play Started" msgstr "" -#: awx/main/models/jobs.py:793 +#: main/models/jobs.py:794 msgid "Playbook Complete" msgstr "" -#: awx/main/models/jobs.py:1237 +#: main/models/jobs.py:1240 msgid "Remove jobs older than a certain number of days" msgstr "" -#: awx/main/models/jobs.py:1238 +#: main/models/jobs.py:1241 msgid "Remove activity stream entries older than a certain number of days" msgstr "" -#: awx/main/models/jobs.py:1239 +#: main/models/jobs.py:1242 msgid "Purge and/or reduce the granularity of system tracking data" msgstr "" -#: awx/main/models/label.py:29 +#: main/models/label.py:29 msgid "Organization this label belongs to." msgstr "" -#: awx/main/models/notifications.py:31 +#: main/models/notifications.py:31 msgid "Email" msgstr "" -#: awx/main/models/notifications.py:32 +#: main/models/notifications.py:32 msgid "Slack" msgstr "" -#: awx/main/models/notifications.py:33 +#: main/models/notifications.py:33 msgid "Twilio" msgstr "" -#: awx/main/models/notifications.py:34 +#: main/models/notifications.py:34 msgid "Pagerduty" msgstr "" -#: awx/main/models/notifications.py:35 +#: main/models/notifications.py:35 msgid "HipChat" msgstr "" -#: awx/main/models/notifications.py:36 +#: main/models/notifications.py:36 msgid "Webhook" msgstr "" -#: awx/main/models/notifications.py:37 +#: main/models/notifications.py:37 msgid "IRC" msgstr "" -#: awx/main/models/notifications.py:127 awx/main/models/unified_jobs.py:57 +#: main/models/notifications.py:127 main/models/unified_jobs.py:57 msgid "Pending" msgstr "" -#: awx/main/models/notifications.py:128 awx/main/models/unified_jobs.py:60 +#: main/models/notifications.py:128 main/models/unified_jobs.py:60 msgid "Successful" msgstr "" -#: awx/main/models/notifications.py:129 awx/main/models/unified_jobs.py:61 +#: main/models/notifications.py:129 main/models/unified_jobs.py:61 msgid "Failed" msgstr "" -#: awx/main/models/organization.py:157 +#: main/models/organization.py:157 msgid "Execute Commands on the Inventory" msgstr "" -#: awx/main/models/organization.py:211 +#: main/models/organization.py:211 msgid "Token not invalidated" msgstr "" -#: awx/main/models/organization.py:212 +#: main/models/organization.py:212 msgid "Token is expired" msgstr "" -#: awx/main/models/organization.py:213 +#: main/models/organization.py:213 msgid "Maximum per-user sessions reached" msgstr "" -#: awx/main/models/organization.py:216 +#: main/models/organization.py:216 msgid "Invalid token" msgstr "" -#: awx/main/models/organization.py:233 +#: main/models/organization.py:233 msgid "Reason the auth token was invalidated." msgstr "" -#: awx/main/models/organization.py:272 +#: main/models/organization.py:272 msgid "Invalid reason specified" msgstr "" -#: awx/main/models/projects.py:43 +#: main/models/projects.py:43 msgid "Git" msgstr "" -#: awx/main/models/projects.py:44 +#: main/models/projects.py:44 msgid "Mercurial" msgstr "" -#: awx/main/models/projects.py:45 +#: main/models/projects.py:45 msgid "Subversion" msgstr "" -#: awx/main/models/projects.py:71 +#: main/models/projects.py:71 msgid "" "Local path (relative to PROJECTS_ROOT) containing playbooks and related " "files for this project." msgstr "" -#: awx/main/models/projects.py:80 +#: main/models/projects.py:80 msgid "SCM Type" msgstr "" -#: awx/main/models/projects.py:86 +#: main/models/projects.py:81 +msgid "Specifies the source control system used to store the project." +msgstr "" + +#: main/models/projects.py:87 msgid "SCM URL" msgstr "" -#: awx/main/models/projects.py:92 +#: main/models/projects.py:88 +msgid "The location where the project is stored." +msgstr "" + +#: main/models/projects.py:94 msgid "SCM Branch" msgstr "" -#: awx/main/models/projects.py:93 +#: main/models/projects.py:95 msgid "Specific branch, tag or commit to checkout." msgstr "" -#: awx/main/models/projects.py:125 +#: main/models/projects.py:99 +msgid "Discard any local changes before syncing the project." +msgstr "" + +#: main/models/projects.py:103 +msgid "Delete the project before syncing." +msgstr "" + +#: main/models/projects.py:116 +msgid "The amount of time to run before the task is canceled." +msgstr "" + +#: main/models/projects.py:130 msgid "Invalid SCM URL." msgstr "" -#: awx/main/models/projects.py:128 +#: main/models/projects.py:133 msgid "SCM URL is required." msgstr "" -#: awx/main/models/projects.py:137 +#: main/models/projects.py:142 msgid "Credential kind must be 'scm'." msgstr "" -#: awx/main/models/projects.py:152 +#: main/models/projects.py:157 msgid "Invalid credential." msgstr "" -#: awx/main/models/projects.py:236 +#: main/models/projects.py:229 +msgid "Update the project when a job is launched that uses the project." +msgstr "" + +#: main/models/projects.py:234 +msgid "" +"The number of seconds after the last project update ran that a newproject " +"update will be launched as a job dependency." +msgstr "" + +#: main/models/projects.py:244 msgid "The last revision fetched by a project update" msgstr "" -#: awx/main/models/projects.py:243 +#: main/models/projects.py:251 msgid "Playbook Files" msgstr "" -#: awx/main/models/projects.py:244 +#: main/models/projects.py:252 msgid "List of playbooks found in the project" msgstr "" -#: awx/main/models/rbac.py:122 +#: main/models/rbac.py:122 msgid "roles" msgstr "" -#: awx/main/models/rbac.py:435 +#: main/models/rbac.py:438 msgid "role_ancestors" msgstr "" -#: awx/main/models/unified_jobs.py:56 +#: main/models/schedules.py:69 +msgid "Enables processing of this schedule by Tower." +msgstr "" + +#: main/models/schedules.py:75 +msgid "The first occurrence of the schedule occurs on or after this time." +msgstr "" + +#: main/models/schedules.py:81 +msgid "" +"The last occurrence of the schedule occurs before this time, aftewards the " +"schedule expires." +msgstr "" + +#: main/models/schedules.py:85 +msgid "A value representing the schedules iCal recurrence rule." +msgstr "" + +#: main/models/schedules.py:91 +msgid "The next time that the scheduled action will run." +msgstr "" + +#: main/models/unified_jobs.py:56 msgid "New" msgstr "" -#: awx/main/models/unified_jobs.py:58 +#: main/models/unified_jobs.py:58 msgid "Waiting" msgstr "" -#: awx/main/models/unified_jobs.py:59 +#: main/models/unified_jobs.py:59 msgid "Running" msgstr "" -#: awx/main/models/unified_jobs.py:63 +#: main/models/unified_jobs.py:63 msgid "Canceled" msgstr "" -#: awx/main/models/unified_jobs.py:67 +#: main/models/unified_jobs.py:67 msgid "Never Updated" msgstr "" -#: awx/main/models/unified_jobs.py:71 awx/ui/templates/ui/index.html:85 -#: awx/ui/templates/ui/index.html.py:104 +#: main/models/unified_jobs.py:71 ui/templates/ui/index.html:85 +#: ui/templates/ui/index.html.py:104 msgid "OK" msgstr "" -#: awx/main/models/unified_jobs.py:72 +#: main/models/unified_jobs.py:72 msgid "Missing" msgstr "" -#: awx/main/models/unified_jobs.py:76 +#: main/models/unified_jobs.py:76 msgid "No External Source" msgstr "" -#: awx/main/models/unified_jobs.py:83 +#: main/models/unified_jobs.py:83 msgid "Updating" msgstr "" -#: awx/main/models/unified_jobs.py:384 +#: main/models/unified_jobs.py:387 msgid "Relaunch" msgstr "" -#: awx/main/models/unified_jobs.py:385 +#: main/models/unified_jobs.py:388 msgid "Callback" msgstr "" -#: awx/main/models/unified_jobs.py:386 +#: main/models/unified_jobs.py:389 msgid "Scheduled" msgstr "" -#: awx/main/models/unified_jobs.py:387 +#: main/models/unified_jobs.py:390 msgid "Dependency" msgstr "" -#: awx/main/models/unified_jobs.py:388 +#: main/models/unified_jobs.py:391 msgid "Workflow" msgstr "" -#: awx/main/notifications/base.py:17 awx/main/notifications/email_backend.py:28 +#: main/models/unified_jobs.py:437 +msgid "The Tower node the job executed on." +msgstr "" + +#: main/models/unified_jobs.py:463 +msgid "The date and time the job was queued for starting." +msgstr "" + +#: main/models/unified_jobs.py:469 +msgid "The date and time the job finished execution." +msgstr "" + +#: main/models/unified_jobs.py:475 +msgid "Elapsed time in seconds that the job ran." +msgstr "" + +#: main/models/unified_jobs.py:497 +msgid "" +"A status field to indicate the state of the job if it wasn't able to run and " +"capture stdout" +msgstr "" + +#: main/notifications/base.py:17 main/notifications/email_backend.py:28 msgid "" "{} #{} had status {} on Ansible Tower, view details at {}\n" "\n" msgstr "" -#: awx/main/notifications/hipchat_backend.py:46 +#: main/notifications/hipchat_backend.py:46 msgid "Error sending messages: {}" msgstr "" -#: awx/main/notifications/hipchat_backend.py:48 +#: main/notifications/hipchat_backend.py:48 msgid "Error sending message to hipchat: {}" msgstr "" -#: awx/main/notifications/irc_backend.py:54 +#: main/notifications/irc_backend.py:54 msgid "Exception connecting to irc server: {}" msgstr "" -#: awx/main/notifications/pagerduty_backend.py:39 +#: main/notifications/pagerduty_backend.py:39 msgid "Exception connecting to PagerDuty: {}" msgstr "" -#: awx/main/notifications/pagerduty_backend.py:48 -#: awx/main/notifications/slack_backend.py:52 -#: awx/main/notifications/twilio_backend.py:46 +#: main/notifications/pagerduty_backend.py:48 +#: main/notifications/slack_backend.py:52 +#: main/notifications/twilio_backend.py:46 msgid "Exception sending messages: {}" msgstr "" -#: awx/main/notifications/twilio_backend.py:36 +#: main/notifications/twilio_backend.py:36 msgid "Exception connecting to Twilio: {}" msgstr "" -#: awx/main/notifications/webhook_backend.py:38 -#: awx/main/notifications/webhook_backend.py:40 +#: main/notifications/webhook_backend.py:38 +#: main/notifications/webhook_backend.py:40 msgid "Error sending notification webhook: {}" msgstr "" -#: awx/main/tasks.py:119 +#: main/tasks.py:139 msgid "Ansible Tower host usage over 90%" msgstr "" -#: awx/main/tasks.py:124 +#: main/tasks.py:144 msgid "Ansible Tower license will expire soon" msgstr "" -#: awx/main/tasks.py:177 +#: main/tasks.py:197 msgid "status_str must be either succeeded or failed" msgstr "" -#: awx/main/utils.py:88 +#: main/utils/common.py:88 #, python-format msgid "Unable to convert \"%s\" to boolean" msgstr "" -#: awx/main/utils.py:242 +#: main/utils/common.py:242 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "" -#: awx/main/utils.py:249 awx/main/utils.py:261 awx/main/utils.py:280 +#: main/utils/common.py:249 main/utils/common.py:261 main/utils/common.py:280 #, python-format msgid "Invalid %s URL" msgstr "" -#: awx/main/utils.py:251 awx/main/utils.py:289 +#: main/utils/common.py:251 main/utils/common.py:289 #, python-format msgid "Unsupported %s URL" msgstr "" -#: awx/main/utils.py:291 +#: main/utils/common.py:291 #, python-format msgid "Unsupported host \"%s\" for file:// URL" msgstr "" -#: awx/main/utils.py:293 +#: main/utils/common.py:293 #, python-format msgid "Host is required for %s URL" msgstr "" -#: awx/main/utils.py:311 +#: main/utils/common.py:311 #, python-format msgid "Username must be \"git\" for SSH access to %s." msgstr "" -#: awx/main/utils.py:317 +#: main/utils/common.py:317 #, python-format msgid "Username must be \"hg\" for SSH access to %s." msgstr "" -#: awx/main/validators.py:60 +#: main/validators.py:60 #, python-format msgid "Invalid certificate or key: %r..." msgstr "" -#: awx/main/validators.py:74 +#: main/validators.py:74 #, python-format msgid "Invalid private key: unsupported type \"%s\"" msgstr "" -#: awx/main/validators.py:78 +#: main/validators.py:78 #, python-format msgid "Unsupported PEM object type: \"%s\"" msgstr "" -#: awx/main/validators.py:103 +#: main/validators.py:103 msgid "Invalid base64-encoded data" msgstr "" -#: awx/main/validators.py:122 +#: main/validators.py:122 msgid "Exactly one private key is required." msgstr "" -#: awx/main/validators.py:124 +#: main/validators.py:124 msgid "At least one private key is required." msgstr "" -#: awx/main/validators.py:126 +#: main/validators.py:126 #, python-format msgid "" "At least %(min_keys)d private keys are required, only %(key_count)d provided." msgstr "" -#: awx/main/validators.py:129 +#: main/validators.py:129 #, python-format msgid "Only one private key is allowed, %(key_count)d provided." msgstr "" -#: awx/main/validators.py:131 +#: main/validators.py:131 #, python-format msgid "" "No more than %(max_keys)d private keys are allowed, %(key_count)d provided." msgstr "" -#: awx/main/validators.py:136 +#: main/validators.py:136 msgid "Exactly one certificate is required." msgstr "" -#: awx/main/validators.py:138 +#: main/validators.py:138 msgid "At least one certificate is required." msgstr "" -#: awx/main/validators.py:140 +#: main/validators.py:140 #, python-format msgid "" "At least %(min_certs)d certificates are required, only %(cert_count)d " "provided." msgstr "" -#: awx/main/validators.py:143 +#: main/validators.py:143 #, python-format msgid "Only one certificate is allowed, %(cert_count)d provided." msgstr "" -#: awx/main/validators.py:145 +#: main/validators.py:145 #, python-format msgid "" "No more than %(max_certs)d certificates are allowed, %(cert_count)d provided." msgstr "" -#: awx/main/views.py:20 +#: main/views.py:20 msgid "API Error" msgstr "" -#: awx/main/views.py:49 +#: main/views.py:49 msgid "Bad Request" msgstr "" -#: awx/main/views.py:50 +#: main/views.py:50 msgid "The request could not be understood by the server." msgstr "" -#: awx/main/views.py:57 +#: main/views.py:57 msgid "Forbidden" msgstr "" -#: awx/main/views.py:58 +#: main/views.py:58 msgid "You don't have permission to access the requested resource." msgstr "" -#: awx/main/views.py:65 +#: main/views.py:65 msgid "Not Found" msgstr "" -#: awx/main/views.py:66 +#: main/views.py:66 msgid "The requested resource could not be found." msgstr "" -#: awx/main/views.py:73 +#: main/views.py:73 msgid "Server Error" msgstr "" -#: awx/main/views.py:74 +#: main/views.py:74 msgid "A server error has occurred." msgstr "" -#: awx/settings/defaults.py:593 +#: settings/defaults.py:600 msgid "Chicago" msgstr "" -#: awx/settings/defaults.py:594 +#: settings/defaults.py:601 msgid "Dallas/Ft. Worth" msgstr "" -#: awx/settings/defaults.py:595 +#: settings/defaults.py:602 msgid "Northern Virginia" msgstr "" -#: awx/settings/defaults.py:596 +#: settings/defaults.py:603 msgid "London" msgstr "" -#: awx/settings/defaults.py:597 +#: settings/defaults.py:604 msgid "Sydney" msgstr "" -#: awx/settings/defaults.py:598 +#: settings/defaults.py:605 msgid "Hong Kong" msgstr "" -#: awx/settings/defaults.py:625 +#: settings/defaults.py:632 msgid "US East (Northern Virginia)" msgstr "" -#: awx/settings/defaults.py:626 +#: settings/defaults.py:633 msgid "US East (Ohio)" msgstr "" -#: awx/settings/defaults.py:627 +#: settings/defaults.py:634 msgid "US West (Oregon)" msgstr "" -#: awx/settings/defaults.py:628 +#: settings/defaults.py:635 msgid "US West (Northern California)" msgstr "" -#: awx/settings/defaults.py:629 +#: settings/defaults.py:636 msgid "EU (Frankfurt)" msgstr "" -#: awx/settings/defaults.py:630 +#: settings/defaults.py:637 msgid "EU (Ireland)" msgstr "" -#: awx/settings/defaults.py:631 +#: settings/defaults.py:638 msgid "Asia Pacific (Singapore)" msgstr "" -#: awx/settings/defaults.py:632 +#: settings/defaults.py:639 msgid "Asia Pacific (Sydney)" msgstr "" -#: awx/settings/defaults.py:633 +#: settings/defaults.py:640 msgid "Asia Pacific (Tokyo)" msgstr "" -#: awx/settings/defaults.py:634 +#: settings/defaults.py:641 msgid "Asia Pacific (Seoul)" msgstr "" -#: awx/settings/defaults.py:635 +#: settings/defaults.py:642 msgid "Asia Pacific (Mumbai)" msgstr "" -#: awx/settings/defaults.py:636 +#: settings/defaults.py:643 msgid "South America (Sao Paulo)" msgstr "" -#: awx/settings/defaults.py:637 +#: settings/defaults.py:644 msgid "US West (GovCloud)" msgstr "" -#: awx/settings/defaults.py:638 +#: settings/defaults.py:645 msgid "China (Beijing)" msgstr "" -#: awx/settings/defaults.py:687 +#: settings/defaults.py:694 msgid "US East (B)" msgstr "" -#: awx/settings/defaults.py:688 +#: settings/defaults.py:695 msgid "US East (C)" msgstr "" -#: awx/settings/defaults.py:689 +#: settings/defaults.py:696 msgid "US East (D)" msgstr "" -#: awx/settings/defaults.py:690 +#: settings/defaults.py:697 msgid "US Central (A)" msgstr "" -#: awx/settings/defaults.py:691 +#: settings/defaults.py:698 msgid "US Central (B)" msgstr "" -#: awx/settings/defaults.py:692 +#: settings/defaults.py:699 msgid "US Central (C)" msgstr "" -#: awx/settings/defaults.py:693 +#: settings/defaults.py:700 msgid "US Central (F)" msgstr "" -#: awx/settings/defaults.py:694 +#: settings/defaults.py:701 msgid "Europe West (B)" msgstr "" -#: awx/settings/defaults.py:695 +#: settings/defaults.py:702 msgid "Europe West (C)" msgstr "" -#: awx/settings/defaults.py:696 +#: settings/defaults.py:703 msgid "Europe West (D)" msgstr "" -#: awx/settings/defaults.py:697 +#: settings/defaults.py:704 msgid "Asia East (A)" msgstr "" -#: awx/settings/defaults.py:698 +#: settings/defaults.py:705 msgid "Asia East (B)" msgstr "" -#: awx/settings/defaults.py:699 +#: settings/defaults.py:706 msgid "Asia East (C)" msgstr "" -#: awx/settings/defaults.py:723 +#: settings/defaults.py:730 msgid "US Central" msgstr "" -#: awx/settings/defaults.py:724 +#: settings/defaults.py:731 msgid "US East" msgstr "" -#: awx/settings/defaults.py:725 +#: settings/defaults.py:732 msgid "US East 2" msgstr "" -#: awx/settings/defaults.py:726 +#: settings/defaults.py:733 msgid "US North Central" msgstr "" -#: awx/settings/defaults.py:727 +#: settings/defaults.py:734 msgid "US South Central" msgstr "" -#: awx/settings/defaults.py:728 +#: settings/defaults.py:735 msgid "US West" msgstr "" -#: awx/settings/defaults.py:729 +#: settings/defaults.py:736 msgid "Europe North" msgstr "" -#: awx/settings/defaults.py:730 +#: settings/defaults.py:737 msgid "Europe West" msgstr "" -#: awx/settings/defaults.py:731 +#: settings/defaults.py:738 msgid "Asia Pacific East" msgstr "" -#: awx/settings/defaults.py:732 +#: settings/defaults.py:739 msgid "Asia Pacific Southeast" msgstr "" -#: awx/settings/defaults.py:733 +#: settings/defaults.py:740 msgid "Japan East" msgstr "" -#: awx/settings/defaults.py:734 +#: settings/defaults.py:741 msgid "Japan West" msgstr "" -#: awx/settings/defaults.py:735 +#: settings/defaults.py:742 msgid "Brazil South" msgstr "" -#: awx/sso/apps.py:9 +#: sso/apps.py:9 msgid "Single Sign-On" msgstr "" -#: awx/sso/conf.py:27 +#: sso/conf.py:27 msgid "" "Mapping to organization admins/users from social auth accounts. This " "setting\n" @@ -2546,7 +2710,7 @@ msgid "" " remove_admins." msgstr "" -#: awx/sso/conf.py:76 +#: sso/conf.py:76 msgid "" "Mapping of team members (users) from social auth accounts. Keys are team\n" "names (will be created if not present). Values are dictionaries of options\n" @@ -2575,40 +2739,40 @@ msgid "" " the rules above will be removed from the team." msgstr "" -#: awx/sso/conf.py:119 +#: sso/conf.py:119 msgid "Authentication Backends" msgstr "" -#: awx/sso/conf.py:120 +#: sso/conf.py:120 msgid "" "List of authentication backends that are enabled based on license features " "and other authentication settings." msgstr "" -#: awx/sso/conf.py:133 +#: sso/conf.py:133 msgid "Social Auth Organization Map" msgstr "" -#: awx/sso/conf.py:145 +#: sso/conf.py:145 msgid "Social Auth Team Map" msgstr "" -#: awx/sso/conf.py:157 +#: sso/conf.py:157 msgid "Social Auth User Fields" msgstr "" -#: awx/sso/conf.py:158 +#: sso/conf.py:158 msgid "" "When set to an empty list `[]`, this setting prevents new user accounts from " "being created. Only users who have previously logged in using social auth or " "have a user account with a matching email address will be able to login." msgstr "" -#: awx/sso/conf.py:176 +#: sso/conf.py:176 msgid "LDAP Server URI" msgstr "" -#: awx/sso/conf.py:177 +#: sso/conf.py:177 msgid "" "URI to connect to LDAP server, such as \"ldap://ldap.example.com:389\" (non-" "SSL) or \"ldaps://ldap.example.com:636\" (SSL). Multiple LDAP servers may be " @@ -2616,19 +2780,18 @@ msgid "" "disabled if this parameter is empty." msgstr "" -#: awx/sso/conf.py:181 awx/sso/conf.py:199 awx/sso/conf.py:211 -#: awx/sso/conf.py:222 awx/sso/conf.py:238 awx/sso/conf.py:257 -#: awx/sso/conf.py:278 awx/sso/conf.py:294 awx/sso/conf.py:313 -#: awx/sso/conf.py:330 awx/sso/conf.py:345 awx/sso/conf.py:360 -#: awx/sso/conf.py:377 awx/sso/conf.py:415 awx/sso/conf.py:456 +#: sso/conf.py:181 sso/conf.py:199 sso/conf.py:211 sso/conf.py:223 +#: sso/conf.py:239 sso/conf.py:258 sso/conf.py:279 sso/conf.py:295 +#: sso/conf.py:314 sso/conf.py:331 sso/conf.py:347 sso/conf.py:362 +#: sso/conf.py:379 sso/conf.py:417 sso/conf.py:458 msgid "LDAP" msgstr "" -#: awx/sso/conf.py:193 +#: sso/conf.py:193 msgid "LDAP Bind DN" msgstr "" -#: awx/sso/conf.py:194 +#: sso/conf.py:194 msgid "" "DN (Distinguished Name) of user to bind for all search queries. Normally in " "the format \"CN=Some User,OU=Users,DC=example,DC=com\" but may also be " @@ -2636,27 +2799,27 @@ msgid "" "user account we will use to login to query LDAP for other user information." msgstr "" -#: awx/sso/conf.py:209 +#: sso/conf.py:209 msgid "LDAP Bind Password" msgstr "" -#: awx/sso/conf.py:210 +#: sso/conf.py:210 msgid "Password used to bind LDAP user account." msgstr "" -#: awx/sso/conf.py:220 +#: sso/conf.py:221 msgid "LDAP Start TLS" msgstr "" -#: awx/sso/conf.py:221 +#: sso/conf.py:222 msgid "Whether to enable TLS when the LDAP connection is not using SSL." msgstr "" -#: awx/sso/conf.py:231 +#: sso/conf.py:232 msgid "LDAP Connection Options" msgstr "" -#: awx/sso/conf.py:232 +#: sso/conf.py:233 msgid "" "Additional options to set for the LDAP connection. LDAP referrals are " "disabled by default (to prevent certain LDAP queries from hanging with AD). " @@ -2665,11 +2828,11 @@ msgid "" "values that can be set." msgstr "" -#: awx/sso/conf.py:250 +#: sso/conf.py:251 msgid "LDAP User Search" msgstr "" -#: awx/sso/conf.py:251 +#: sso/conf.py:252 msgid "" "LDAP search query to find users. Any user that matches the given pattern " "will be able to login to Tower. The user should also be mapped into an " @@ -2678,11 +2841,11 @@ msgid "" "possible. See python-ldap documentation as linked at the top of this section." msgstr "" -#: awx/sso/conf.py:272 +#: sso/conf.py:273 msgid "LDAP User DN Template" msgstr "" -#: awx/sso/conf.py:273 +#: sso/conf.py:274 msgid "" "Alternative to user search, if user DNs are all of the same format. This " "approach will be more efficient for user lookups than searching if it is " @@ -2690,11 +2853,11 @@ msgid "" "will be used instead of AUTH_LDAP_USER_SEARCH." msgstr "" -#: awx/sso/conf.py:288 +#: sso/conf.py:289 msgid "LDAP User Attribute Map" msgstr "" -#: awx/sso/conf.py:289 +#: sso/conf.py:290 msgid "" "Mapping of LDAP user schema to Tower API user attributes (key is user " "attribute name, value is LDAP attribute name). The default setting is valid " @@ -2702,54 +2865,54 @@ msgid "" "change the values (not the keys) of the dictionary/hash-table." msgstr "" -#: awx/sso/conf.py:308 +#: sso/conf.py:309 msgid "LDAP Group Search" msgstr "" -#: awx/sso/conf.py:309 +#: sso/conf.py:310 msgid "" "Users in Tower are mapped to organizations based on their membership in LDAP " "groups. This setting defines the LDAP search query to find groups. Note that " "this, unlike the user search above, does not support LDAPSearchUnion." msgstr "" -#: awx/sso/conf.py:326 +#: sso/conf.py:327 msgid "LDAP Group Type" msgstr "" -#: awx/sso/conf.py:327 +#: sso/conf.py:328 msgid "" "The group type may need to be changed based on the type of the LDAP server. " "Values are listed at: http://pythonhosted.org/django-auth-ldap/groups." "html#types-of-groups" msgstr "" -#: awx/sso/conf.py:340 +#: sso/conf.py:342 msgid "LDAP Require Group" msgstr "" -#: awx/sso/conf.py:341 +#: sso/conf.py:343 msgid "" "Group DN required to login. If specified, user must be a member of this " "group to login via LDAP. If not set, everyone in LDAP that matches the user " "search will be able to login via Tower. Only one require group is supported." msgstr "" -#: awx/sso/conf.py:356 +#: sso/conf.py:358 msgid "LDAP Deny Group" msgstr "" -#: awx/sso/conf.py:357 +#: sso/conf.py:359 msgid "" "Group DN denied from login. If specified, user will not be allowed to login " "if a member of this group. Only one deny group is supported." msgstr "" -#: awx/sso/conf.py:370 +#: sso/conf.py:372 msgid "LDAP User Flags By Group" msgstr "" -#: awx/sso/conf.py:371 +#: sso/conf.py:373 msgid "" "User profile flags updated from group membership (key is user attribute " "name, value is group DN). These are boolean fields that are matched based " @@ -2758,11 +2921,11 @@ msgid "" "false at login time based on current LDAP settings." msgstr "" -#: awx/sso/conf.py:389 +#: sso/conf.py:391 msgid "LDAP Organization Map" msgstr "" -#: awx/sso/conf.py:390 +#: sso/conf.py:392 msgid "" "Mapping between organization admins/users and LDAP groups. This controls " "what users are placed into what Tower organizations relative to their LDAP " @@ -2789,11 +2952,11 @@ msgid "" "remove_admins." msgstr "" -#: awx/sso/conf.py:438 +#: sso/conf.py:440 msgid "LDAP Team Map" msgstr "" -#: awx/sso/conf.py:439 +#: sso/conf.py:441 msgid "" "Mapping between team members (users) and LDAP groups. Keys are team names " "(will be created if not present). Values are dictionaries of options for " @@ -2812,88 +2975,87 @@ msgid "" "of the given groups will be removed from the team." msgstr "" -#: awx/sso/conf.py:482 +#: sso/conf.py:484 msgid "RADIUS Server" msgstr "" -#: awx/sso/conf.py:483 +#: sso/conf.py:485 msgid "" "Hostname/IP of RADIUS server. RADIUS authentication will be disabled if this " "setting is empty." msgstr "" -#: awx/sso/conf.py:485 awx/sso/conf.py:499 awx/sso/conf.py:511 +#: sso/conf.py:487 sso/conf.py:501 sso/conf.py:513 msgid "RADIUS" msgstr "" -#: awx/sso/conf.py:497 +#: sso/conf.py:499 msgid "RADIUS Port" msgstr "" -#: awx/sso/conf.py:498 +#: sso/conf.py:500 msgid "Port of RADIUS server." msgstr "" -#: awx/sso/conf.py:509 +#: sso/conf.py:511 msgid "RADIUS Secret" msgstr "" -#: awx/sso/conf.py:510 +#: sso/conf.py:512 msgid "Shared secret for authenticating to RADIUS server." msgstr "" -#: awx/sso/conf.py:525 +#: sso/conf.py:528 msgid "Google OAuth2 Callback URL" msgstr "" -#: awx/sso/conf.py:526 +#: sso/conf.py:529 msgid "" "Create a project at https://console.developers.google.com/ to obtain an " "OAuth2 key and secret for a web application. Ensure that the Google+ API is " "enabled. Provide this URL as the callback URL for your application." msgstr "" -#: awx/sso/conf.py:530 awx/sso/conf.py:541 awx/sso/conf.py:552 -#: awx/sso/conf.py:564 awx/sso/conf.py:578 awx/sso/conf.py:590 -#: awx/sso/conf.py:602 +#: sso/conf.py:533 sso/conf.py:544 sso/conf.py:555 sso/conf.py:568 +#: sso/conf.py:582 sso/conf.py:594 sso/conf.py:606 msgid "Google OAuth2" msgstr "" -#: awx/sso/conf.py:539 +#: sso/conf.py:542 msgid "Google OAuth2 Key" msgstr "" -#: awx/sso/conf.py:540 +#: sso/conf.py:543 msgid "" "The OAuth2 key from your web application at https://console.developers." "google.com/." msgstr "" -#: awx/sso/conf.py:550 +#: sso/conf.py:553 msgid "Google OAuth2 Secret" msgstr "" -#: awx/sso/conf.py:551 +#: sso/conf.py:554 msgid "" "The OAuth2 secret from your web application at https://console.developers." "google.com/." msgstr "" -#: awx/sso/conf.py:561 +#: sso/conf.py:565 msgid "Google OAuth2 Whitelisted Domains" msgstr "" -#: awx/sso/conf.py:562 +#: sso/conf.py:566 msgid "" "Update this setting to restrict the domains who are allowed to login using " "Google OAuth2." msgstr "" -#: awx/sso/conf.py:573 +#: sso/conf.py:577 msgid "Google OAuth2 Extra Arguments" msgstr "" -#: awx/sso/conf.py:574 +#: sso/conf.py:578 msgid "" "Extra arguments for Google OAuth2 login. When only allowing a single domain " "to authenticate, set to `{\"hd\": \"yourdomain.com\"}` and Google will not " @@ -2901,60 +3063,60 @@ msgid "" "Google accounts." msgstr "" -#: awx/sso/conf.py:588 +#: sso/conf.py:592 msgid "Google OAuth2 Organization Map" msgstr "" -#: awx/sso/conf.py:600 +#: sso/conf.py:604 msgid "Google OAuth2 Team Map" msgstr "" -#: awx/sso/conf.py:616 +#: sso/conf.py:620 msgid "GitHub OAuth2 Callback URL" msgstr "" -#: awx/sso/conf.py:617 +#: sso/conf.py:621 msgid "" "Create a developer application at https://github.com/settings/developers to " "obtain an OAuth2 key (Client ID) and secret (Client Secret). Provide this " "URL as the callback URL for your application." msgstr "" -#: awx/sso/conf.py:621 awx/sso/conf.py:632 awx/sso/conf.py:642 -#: awx/sso/conf.py:653 awx/sso/conf.py:665 +#: sso/conf.py:625 sso/conf.py:636 sso/conf.py:646 sso/conf.py:658 +#: sso/conf.py:670 msgid "GitHub OAuth2" msgstr "" -#: awx/sso/conf.py:630 +#: sso/conf.py:634 msgid "GitHub OAuth2 Key" msgstr "" -#: awx/sso/conf.py:631 +#: sso/conf.py:635 msgid "The OAuth2 key (Client ID) from your GitHub developer application." msgstr "" -#: awx/sso/conf.py:640 +#: sso/conf.py:644 msgid "GitHub OAuth2 Secret" msgstr "" -#: awx/sso/conf.py:641 +#: sso/conf.py:645 msgid "" "The OAuth2 secret (Client Secret) from your GitHub developer application." msgstr "" -#: awx/sso/conf.py:651 +#: sso/conf.py:656 msgid "GitHub OAuth2 Organization Map" msgstr "" -#: awx/sso/conf.py:663 +#: sso/conf.py:668 msgid "GitHub OAuth2 Team Map" msgstr "" -#: awx/sso/conf.py:679 +#: sso/conf.py:684 msgid "GitHub Organization OAuth2 Callback URL" msgstr "" -#: awx/sso/conf.py:680 awx/sso/conf.py:754 +#: sso/conf.py:685 sso/conf.py:760 msgid "" "Create an organization-owned application at https://github.com/organizations/" "/settings/applications and obtain an OAuth2 key (Client ID) and " @@ -2962,86 +3124,86 @@ msgid "" "application." msgstr "" -#: awx/sso/conf.py:684 awx/sso/conf.py:695 awx/sso/conf.py:705 -#: awx/sso/conf.py:716 awx/sso/conf.py:727 awx/sso/conf.py:739 +#: sso/conf.py:689 sso/conf.py:700 sso/conf.py:710 sso/conf.py:722 +#: sso/conf.py:733 sso/conf.py:745 msgid "GitHub Organization OAuth2" msgstr "" -#: awx/sso/conf.py:693 +#: sso/conf.py:698 msgid "GitHub Organization OAuth2 Key" msgstr "" -#: awx/sso/conf.py:694 awx/sso/conf.py:768 +#: sso/conf.py:699 sso/conf.py:774 msgid "The OAuth2 key (Client ID) from your GitHub organization application." msgstr "" -#: awx/sso/conf.py:703 +#: sso/conf.py:708 msgid "GitHub Organization OAuth2 Secret" msgstr "" -#: awx/sso/conf.py:704 awx/sso/conf.py:778 +#: sso/conf.py:709 sso/conf.py:784 msgid "" "The OAuth2 secret (Client Secret) from your GitHub organization application." msgstr "" -#: awx/sso/conf.py:713 +#: sso/conf.py:719 msgid "GitHub Organization Name" msgstr "" -#: awx/sso/conf.py:714 +#: sso/conf.py:720 msgid "" "The name of your GitHub organization, as used in your organization's URL: " "https://github.com//." msgstr "" -#: awx/sso/conf.py:725 +#: sso/conf.py:731 msgid "GitHub Organization OAuth2 Organization Map" msgstr "" -#: awx/sso/conf.py:737 +#: sso/conf.py:743 msgid "GitHub Organization OAuth2 Team Map" msgstr "" -#: awx/sso/conf.py:753 +#: sso/conf.py:759 msgid "GitHub Team OAuth2 Callback URL" msgstr "" -#: awx/sso/conf.py:758 awx/sso/conf.py:769 awx/sso/conf.py:779 -#: awx/sso/conf.py:790 awx/sso/conf.py:801 awx/sso/conf.py:813 +#: sso/conf.py:764 sso/conf.py:775 sso/conf.py:785 sso/conf.py:797 +#: sso/conf.py:808 sso/conf.py:820 msgid "GitHub Team OAuth2" msgstr "" -#: awx/sso/conf.py:767 +#: sso/conf.py:773 msgid "GitHub Team OAuth2 Key" msgstr "" -#: awx/sso/conf.py:777 +#: sso/conf.py:783 msgid "GitHub Team OAuth2 Secret" msgstr "" -#: awx/sso/conf.py:787 +#: sso/conf.py:794 msgid "GitHub Team ID" msgstr "" -#: awx/sso/conf.py:788 +#: sso/conf.py:795 msgid "" "Find the numeric team ID using the Github API: http://fabian-kostadinov." "github.io/2015/01/16/how-to-find-a-github-team-id/." msgstr "" -#: awx/sso/conf.py:799 +#: sso/conf.py:806 msgid "GitHub Team OAuth2 Organization Map" msgstr "" -#: awx/sso/conf.py:811 +#: sso/conf.py:818 msgid "GitHub Team OAuth2 Team Map" msgstr "" -#: awx/sso/conf.py:827 +#: sso/conf.py:834 msgid "Azure AD OAuth2 Callback URL" msgstr "" -#: awx/sso/conf.py:828 +#: sso/conf.py:835 msgid "" "Register an Azure AD application as described by https://msdn.microsoft.com/" "en-us/library/azure/dn132599.aspx and obtain an OAuth2 key (Client ID) and " @@ -3049,118 +3211,117 @@ msgid "" "application." msgstr "" -#: awx/sso/conf.py:832 awx/sso/conf.py:843 awx/sso/conf.py:853 -#: awx/sso/conf.py:864 awx/sso/conf.py:876 +#: sso/conf.py:839 sso/conf.py:850 sso/conf.py:860 sso/conf.py:872 +#: sso/conf.py:884 msgid "Azure AD OAuth2" msgstr "" -#: awx/sso/conf.py:841 +#: sso/conf.py:848 msgid "Azure AD OAuth2 Key" msgstr "" -#: awx/sso/conf.py:842 +#: sso/conf.py:849 msgid "The OAuth2 key (Client ID) from your Azure AD application." msgstr "" -#: awx/sso/conf.py:851 +#: sso/conf.py:858 msgid "Azure AD OAuth2 Secret" msgstr "" -#: awx/sso/conf.py:852 +#: sso/conf.py:859 msgid "The OAuth2 secret (Client Secret) from your Azure AD application." msgstr "" -#: awx/sso/conf.py:862 +#: sso/conf.py:870 msgid "Azure AD OAuth2 Organization Map" msgstr "" -#: awx/sso/conf.py:874 +#: sso/conf.py:882 msgid "Azure AD OAuth2 Team Map" msgstr "" -#: awx/sso/conf.py:895 +#: sso/conf.py:903 msgid "SAML Service Provider Callback URL" msgstr "" -#: awx/sso/conf.py:896 +#: sso/conf.py:904 msgid "" "Register Tower as a service provider (SP) with each identity provider (IdP) " "you have configured. Provide your SP Entity ID and this callback URL for " "your application." msgstr "" -#: awx/sso/conf.py:899 awx/sso/conf.py:913 awx/sso/conf.py:927 -#: awx/sso/conf.py:941 awx/sso/conf.py:955 awx/sso/conf.py:972 -#: awx/sso/conf.py:994 awx/sso/conf.py:1013 awx/sso/conf.py:1033 -#: awx/sso/conf.py:1067 awx/sso/conf.py:1080 +#: sso/conf.py:907 sso/conf.py:921 sso/conf.py:934 sso/conf.py:948 +#: sso/conf.py:962 sso/conf.py:980 sso/conf.py:1002 sso/conf.py:1021 +#: sso/conf.py:1041 sso/conf.py:1075 sso/conf.py:1088 msgid "SAML" msgstr "" -#: awx/sso/conf.py:910 +#: sso/conf.py:918 msgid "SAML Service Provider Metadata URL" msgstr "" -#: awx/sso/conf.py:911 +#: sso/conf.py:919 msgid "" "If your identity provider (IdP) allows uploading an XML metadata file, you " "can download one from this URL." msgstr "" -#: awx/sso/conf.py:924 +#: sso/conf.py:931 msgid "SAML Service Provider Entity ID" msgstr "" -#: awx/sso/conf.py:925 +#: sso/conf.py:932 msgid "" -"Set to a URL for a domain name you own (does not need to be a valid URL; " -"only used as a unique ID)." +"The application-defined unique identifier used as the audience of the SAML " +"service provider (SP) configuration." msgstr "" -#: awx/sso/conf.py:938 +#: sso/conf.py:945 msgid "SAML Service Provider Public Certificate" msgstr "" -#: awx/sso/conf.py:939 +#: sso/conf.py:946 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the " "certificate content here." msgstr "" -#: awx/sso/conf.py:952 +#: sso/conf.py:959 msgid "SAML Service Provider Private Key" msgstr "" -#: awx/sso/conf.py:953 +#: sso/conf.py:960 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the " "private key content here." msgstr "" -#: awx/sso/conf.py:970 +#: sso/conf.py:978 msgid "SAML Service Provider Organization Info" msgstr "" -#: awx/sso/conf.py:971 +#: sso/conf.py:979 msgid "Configure this setting with information about your app." msgstr "" -#: awx/sso/conf.py:992 +#: sso/conf.py:1000 msgid "SAML Service Provider Technical Contact" msgstr "" -#: awx/sso/conf.py:993 awx/sso/conf.py:1012 +#: sso/conf.py:1001 sso/conf.py:1020 msgid "Configure this setting with your contact information." msgstr "" -#: awx/sso/conf.py:1011 +#: sso/conf.py:1019 msgid "SAML Service Provider Support Contact" msgstr "" -#: awx/sso/conf.py:1026 +#: sso/conf.py:1034 msgid "SAML Enabled Identity Providers" msgstr "" -#: awx/sso/conf.py:1027 +#: sso/conf.py:1035 msgid "" "Configure the Entity ID, SSO URL and certificate for each identity provider " "(IdP) in use. Multiple SAML IdPs are supported. Some IdPs may provide user " @@ -3169,237 +3330,217 @@ msgid "" "Attribute names may be overridden for each IdP." msgstr "" -#: awx/sso/conf.py:1065 +#: sso/conf.py:1073 msgid "SAML Organization Map" msgstr "" -#: awx/sso/conf.py:1078 +#: sso/conf.py:1086 msgid "SAML Team Map" msgstr "" -#: awx/sso/fields.py:123 -#, python-brace-format +#: sso/fields.py:123 msgid "Invalid connection option(s): {invalid_options}." msgstr "" -#: awx/sso/fields.py:182 +#: sso/fields.py:182 msgid "Base" msgstr "" -#: awx/sso/fields.py:183 +#: sso/fields.py:183 msgid "One Level" msgstr "" -#: awx/sso/fields.py:184 +#: sso/fields.py:184 msgid "Subtree" msgstr "" -#: awx/sso/fields.py:202 -#, python-brace-format +#: sso/fields.py:202 msgid "Expected a list of three items but got {length} instead." msgstr "" -#: awx/sso/fields.py:203 -#, python-brace-format +#: sso/fields.py:203 msgid "Expected an instance of LDAPSearch but got {input_type} instead." msgstr "" -#: awx/sso/fields.py:239 -#, python-brace-format +#: sso/fields.py:239 msgid "" "Expected an instance of LDAPSearch or LDAPSearchUnion but got {input_type} " "instead." msgstr "" -#: awx/sso/fields.py:266 -#, python-brace-format +#: sso/fields.py:266 msgid "Invalid user attribute(s): {invalid_attrs}." msgstr "" -#: awx/sso/fields.py:283 -#, python-brace-format +#: sso/fields.py:283 msgid "Expected an instance of LDAPGroupType but got {input_type} instead." msgstr "" -#: awx/sso/fields.py:308 -#, python-brace-format +#: sso/fields.py:308 msgid "Invalid user flag: \"{invalid_flag}\"." msgstr "" -#: awx/sso/fields.py:324 awx/sso/fields.py:491 -#, python-brace-format +#: sso/fields.py:324 sso/fields.py:491 msgid "" "Expected None, True, False, a string or list of strings but got {input_type} " "instead." msgstr "" -#: awx/sso/fields.py:360 -#, python-brace-format +#: sso/fields.py:360 msgid "Missing key(s): {missing_keys}." msgstr "" -#: awx/sso/fields.py:361 -#, python-brace-format +#: sso/fields.py:361 msgid "Invalid key(s): {invalid_keys}." msgstr "" -#: awx/sso/fields.py:410 awx/sso/fields.py:527 -#, python-brace-format +#: sso/fields.py:410 sso/fields.py:527 msgid "Invalid key(s) for organization map: {invalid_keys}." msgstr "" -#: awx/sso/fields.py:428 -#, python-brace-format +#: sso/fields.py:428 msgid "Missing required key for team map: {invalid_keys}." msgstr "" -#: awx/sso/fields.py:429 awx/sso/fields.py:546 -#, python-brace-format +#: sso/fields.py:429 sso/fields.py:546 msgid "Invalid key(s) for team map: {invalid_keys}." msgstr "" -#: awx/sso/fields.py:545 -#, python-brace-format +#: sso/fields.py:545 msgid "Missing required key for team map: {missing_keys}." msgstr "" -#: awx/sso/fields.py:563 -#, python-brace-format +#: sso/fields.py:563 msgid "Missing required key(s) for org info record: {missing_keys}." msgstr "" -#: awx/sso/fields.py:576 -#, python-brace-format +#: sso/fields.py:576 msgid "Invalid language code(s) for org info: {invalid_lang_codes}." msgstr "" -#: awx/sso/fields.py:595 -#, python-brace-format +#: sso/fields.py:595 msgid "Missing required key(s) for contact: {missing_keys}." msgstr "" -#: awx/sso/fields.py:607 -#, python-brace-format +#: sso/fields.py:607 msgid "Missing required key(s) for IdP: {missing_keys}." msgstr "" -#: awx/sso/pipeline.py:24 -#, python-brace-format +#: sso/pipeline.py:24 msgid "An account cannot be found for {0}" msgstr "" -#: awx/sso/pipeline.py:30 +#: sso/pipeline.py:30 msgid "Your account is inactive" msgstr "" -#: awx/sso/validators.py:19 awx/sso/validators.py:44 +#: sso/validators.py:19 sso/validators.py:44 #, python-format msgid "DN must include \"%%(user)s\" placeholder for username: %s" msgstr "" -#: awx/sso/validators.py:26 +#: sso/validators.py:26 #, python-format msgid "Invalid DN: %s" msgstr "" -#: awx/sso/validators.py:56 +#: sso/validators.py:56 #, python-format msgid "Invalid filter: %s" msgstr "" -#: awx/templates/error.html:4 awx/ui/templates/ui/index.html:8 +#: templates/error.html:4 ui/templates/ui/index.html:8 msgid "Ansible Tower" msgstr "" -#: awx/templates/rest_framework/api.html:39 +#: templates/rest_framework/api.html:39 msgid "Ansible Tower API Guide" msgstr "" -#: awx/templates/rest_framework/api.html:40 +#: templates/rest_framework/api.html:40 msgid "Back to Ansible Tower" msgstr "" -#: awx/templates/rest_framework/api.html:41 +#: templates/rest_framework/api.html:41 msgid "Resize" msgstr "" -#: awx/templates/rest_framework/base.html:78 -#: awx/templates/rest_framework/base.html:92 +#: templates/rest_framework/base.html:78 templates/rest_framework/base.html:92 #, python-format msgid "Make a GET request on the %(name)s resource" msgstr "" -#: awx/templates/rest_framework/base.html:80 +#: templates/rest_framework/base.html:80 msgid "Specify a format for the GET request" msgstr "" -#: awx/templates/rest_framework/base.html:86 +#: templates/rest_framework/base.html:86 #, python-format msgid "" "Make a GET request on the %(name)s resource with the format set to `" "%(format)s`" msgstr "" -#: awx/templates/rest_framework/base.html:100 +#: templates/rest_framework/base.html:100 #, python-format msgid "Make an OPTIONS request on the %(name)s resource" msgstr "" -#: awx/templates/rest_framework/base.html:106 +#: templates/rest_framework/base.html:106 #, python-format msgid "Make a DELETE request on the %(name)s resource" msgstr "" -#: awx/templates/rest_framework/base.html:113 +#: templates/rest_framework/base.html:113 msgid "Filters" msgstr "" -#: awx/templates/rest_framework/base.html:172 -#: awx/templates/rest_framework/base.html:186 +#: templates/rest_framework/base.html:172 +#: templates/rest_framework/base.html:186 #, python-format msgid "Make a POST request on the %(name)s resource" msgstr "" -#: awx/templates/rest_framework/base.html:216 -#: awx/templates/rest_framework/base.html:230 +#: templates/rest_framework/base.html:216 +#: templates/rest_framework/base.html:230 #, python-format msgid "Make a PUT request on the %(name)s resource" msgstr "" -#: awx/templates/rest_framework/base.html:233 +#: templates/rest_framework/base.html:233 #, python-format msgid "Make a PATCH request on the %(name)s resource" msgstr "" -#: awx/ui/apps.py:9 awx/ui/conf.py:22 awx/ui/conf.py:38 awx/ui/conf.py:53 +#: ui/apps.py:9 ui/conf.py:22 ui/conf.py:38 ui/conf.py:53 msgid "UI" msgstr "" -#: awx/ui/conf.py:16 +#: ui/conf.py:16 msgid "Off" msgstr "" -#: awx/ui/conf.py:17 +#: ui/conf.py:17 msgid "Anonymous" msgstr "" -#: awx/ui/conf.py:18 +#: ui/conf.py:18 msgid "Detailed" msgstr "" -#: awx/ui/conf.py:20 +#: ui/conf.py:20 msgid "Analytics Tracking State" msgstr "" -#: awx/ui/conf.py:21 +#: ui/conf.py:21 msgid "Enable or Disable Analytics Tracking." msgstr "" -#: awx/ui/conf.py:31 +#: ui/conf.py:31 msgid "Custom Login Info" msgstr "" -#: awx/ui/conf.py:32 +#: ui/conf.py:32 msgid "" "If needed, you can add specific information (such as a legal notice or a " "disclaimer) to a text box in the login modal using this setting. Any content " @@ -3408,42 +3549,42 @@ msgid "" "(paragraphs) must be escaped as `\\n` within the block of text." msgstr "" -#: awx/ui/conf.py:48 +#: ui/conf.py:48 msgid "Custom Logo" msgstr "" -#: awx/ui/conf.py:49 +#: ui/conf.py:49 msgid "" "To set up a custom logo, provide a file that you create. For the custom logo " "to look its best, use a `.png` file with a transparent background. GIF, PNG " "and JPEG formats are supported." msgstr "" -#: awx/ui/fields.py:29 +#: ui/fields.py:29 msgid "" "Invalid format for custom logo. Must be a data URL with a base64-encoded " "GIF, PNG or JPEG image." msgstr "" -#: awx/ui/fields.py:30 +#: ui/fields.py:30 msgid "Invalid base64-encoded data in data URL." msgstr "" -#: awx/ui/templates/ui/index.html:49 +#: ui/templates/ui/index.html:49 msgid "" "Your session will expire in 60 seconds, would you like to continue?" msgstr "" -#: awx/ui/templates/ui/index.html:64 +#: ui/templates/ui/index.html:64 msgid "CANCEL" msgstr "" -#: awx/ui/templates/ui/index.html:116 +#: ui/templates/ui/index.html:116 msgid "Set how many days of data should be retained." msgstr "" -#: awx/ui/templates/ui/index.html:122 +#: ui/templates/ui/index.html:122 msgid "" "Please enter an integer that is not " @@ -3452,7 +3593,7 @@ msgid "" "span>." msgstr "" -#: awx/ui/templates/ui/index.html:127 +#: ui/templates/ui/index.html:127 msgid "" "For facts collected older than the time period specified, save one fact scan " "(snapshot) per time window (frequency). For example, facts older than 30 " @@ -3464,11 +3605,11 @@ msgid "" "
    " msgstr "" -#: awx/ui/templates/ui/index.html:136 +#: ui/templates/ui/index.html:136 msgid "Select a time period after which to remove old facts" msgstr "" -#: awx/ui/templates/ui/index.html:150 +#: ui/templates/ui/index.html:150 msgid "" "Please enter an integer " @@ -3477,11 +3618,11 @@ msgid "" "that is lower than 9999." msgstr "" -#: awx/ui/templates/ui/index.html:155 +#: ui/templates/ui/index.html:155 msgid "Select a frequency for snapshot retention" msgstr "" -#: awx/ui/templates/ui/index.html:169 +#: ui/templates/ui/index.html:169 msgid "" "Please enter an integer." msgstr "" -#: awx/ui/templates/ui/index.html:175 +#: ui/templates/ui/index.html:175 msgid "working..." msgstr "" diff --git a/awx/ui/po/ansible-tower-ui.pot b/awx/ui/po/ansible-tower-ui.pot index e3226dc3f4..df8bbde43b 100644 --- a/awx/ui/po/ansible-tower-ui.pot +++ b/awx/ui/po/ansible-tower-ui.pot @@ -34,15 +34,15 @@ msgstr "" #: client/src/forms/Inventories.js:153 #: client/src/forms/JobTemplates.js:414 #: client/src/forms/Organizations.js:75 -#: client/src/forms/Projects.js:238 +#: client/src/forms/Projects.js:237 #: client/src/forms/Teams.js:86 -#: client/src/forms/Workflows.js:126 +#: client/src/forms/Workflows.js:127 #: client/src/inventory-scripts/inventory-scripts.list.js:45 #: client/src/lists/Credentials.js:59 #: client/src/lists/Inventories.js:68 #: client/src/lists/Projects.js:67 #: client/src/lists/Teams.js:50 -#: client/src/lists/Templates.js:61 +#: client/src/lists/Templates.js:62 #: client/src/lists/Users.js:58 #: client/src/notifications/notificationTemplates.list.js:52 msgid "ADD" @@ -81,7 +81,7 @@ msgid "Account Token" msgstr "" #: client/src/dashboard/lists/job-templates/job-templates-list.partial.html:20 -#: client/src/shared/list-generator/list-generator.factory.js:502 +#: client/src/shared/list-generator/list-generator.factory.js:538 msgid "Actions" msgstr "" @@ -94,7 +94,7 @@ msgstr "" #: client/src/forms/Inventories.js:150 #: client/src/forms/Organizations.js:72 #: client/src/forms/Teams.js:83 -#: client/src/forms/Workflows.js:123 +#: client/src/forms/Workflows.js:124 msgid "Add" msgstr "" @@ -119,8 +119,8 @@ msgid "Add Project" msgstr "" #: client/src/forms/JobTemplates.js:459 -#: client/src/forms/Workflows.js:171 -#: client/src/shared/form-generator.js:1703 +#: client/src/forms/Workflows.js:172 +#: client/src/shared/form-generator.js:1707 msgid "Add Survey" msgstr "" @@ -136,7 +136,7 @@ msgstr "" #: client/src/forms/Inventories.js:151 #: client/src/forms/JobTemplates.js:412 #: client/src/forms/Organizations.js:73 -#: client/src/forms/Projects.js:236 +#: client/src/forms/Projects.js:235 msgid "Add a permission" msgstr "" @@ -219,6 +219,10 @@ msgstr "" msgid "Authorize Password" msgstr "" +#: client/src/configuration/auth-form/configuration-auth.controller.js:101 +msgid "Azure AD" +msgstr "" + #: client/src/forms/Projects.js:80 msgid "Base path used for locating playbooks. Directories found inside this path will be listed in the playbook directory drop-down. Together the base path and selected playbook directory provide the full path used to locate playbooks." msgstr "" @@ -235,11 +239,11 @@ msgstr "" msgid "CREDENTIALS" msgstr "" -#: client/src/forms/Projects.js:195 +#: client/src/forms/Projects.js:194 msgid "Cache Timeout" msgstr "" -#: client/src/forms/Projects.js:184 +#: client/src/forms/Projects.js:183 msgid "Cache Timeout%s (seconds)%s" msgstr "" @@ -265,7 +269,8 @@ msgstr "" msgid "Call to get project failed. GET status:" msgstr "" -#: client/src/shared/form-generator.js:1691 +#: client/src/configuration/configuration.controller.js:414 +#: client/src/shared/form-generator.js:1695 msgid "Cancel" msgstr "" @@ -281,6 +286,10 @@ msgstr "" msgid "Canceled. Click for details" msgstr "" +#: client/src/forms/Projects.js:82 +msgid "Change %s under \"Configure Tower\" to change this location." +msgstr "" + #: client/src/shared/form-generator.js:1084 msgid "Choose a %s" msgstr "" @@ -289,7 +298,7 @@ msgstr "" msgid "Choose your license file, agree to the End User License Agreement, and click submit." msgstr "" -#: client/src/forms/Projects.js:152 +#: client/src/forms/Projects.js:151 msgid "Clean" msgstr "" @@ -317,7 +326,7 @@ msgstr "" msgid "Client Secret" msgstr "" -#: client/src/shared/form-generator.js:1695 +#: client/src/shared/form-generator.js:1699 msgid "Close" msgstr "" @@ -346,6 +355,14 @@ msgstr "" msgid "Confirm Password" msgstr "" +#: client/src/configuration/configuration.controller.js:421 +msgid "Confirm Reset" +msgstr "" + +#: client/src/configuration/configuration.controller.js:430 +msgid "Confirm factory reset" +msgstr "" + #: client/src/forms/JobTemplates.js:255 #: client/src/forms/JobTemplates.js:273 #: client/src/forms/WorkflowMaker.js:141 @@ -357,11 +374,11 @@ msgstr "" msgid "Control the level of output ansible will produce as the playbook executes." msgstr "" -#: client/src/lists/Templates.js:99 +#: client/src/lists/Templates.js:100 msgid "Copy" msgstr "" -#: client/src/lists/Templates.js:102 +#: client/src/lists/Templates.js:103 msgid "Copy template" msgstr "" @@ -402,7 +419,7 @@ msgstr "" msgid "Create a new team" msgstr "" -#: client/src/lists/Templates.js:59 +#: client/src/lists/Templates.js:60 msgid "Create a new template" msgstr "" @@ -435,7 +452,7 @@ msgstr "" msgid "Custom Script" msgstr "" -#: client/src/app.js:405 +#: client/src/app.js:409 msgid "DASHBOARD" msgstr "" @@ -451,7 +468,7 @@ msgstr "" #: client/src/lists/Credentials.js:90 #: client/src/lists/Inventories.js:92 #: client/src/lists/Teams.js:77 -#: client/src/lists/Templates.js:123 +#: client/src/lists/Templates.js:125 #: client/src/lists/Users.js:87 #: client/src/notifications/notificationTemplates.list.js:89 msgid "Delete" @@ -473,7 +490,7 @@ msgstr "" msgid "Delete notification" msgstr "" -#: client/src/forms/Projects.js:162 +#: client/src/forms/Projects.js:161 msgid "Delete on Update" msgstr "" @@ -481,7 +498,7 @@ msgstr "" msgid "Delete team" msgstr "" -#: client/src/lists/Templates.js:126 +#: client/src/lists/Templates.js:128 msgid "Delete template" msgstr "" @@ -489,7 +506,7 @@ msgstr "" msgid "Delete the job" msgstr "" -#: client/src/forms/Projects.js:164 +#: client/src/forms/Projects.js:163 msgid "Delete the local repository in its entirety prior to performing an update." msgstr "" @@ -505,7 +522,7 @@ msgstr "" msgid "Delete user" msgstr "" -#: client/src/forms/Projects.js:164 +#: client/src/forms/Projects.js:163 msgid "Depending on the size of the repository this may significantly increase the amount of time required to complete an update." msgstr "" @@ -517,7 +534,7 @@ msgstr "" #: client/src/forms/Teams.js:34 #: client/src/forms/Users.js:142 #: client/src/forms/Users.js:167 -#: client/src/forms/Workflows.js:40 +#: client/src/forms/Workflows.js:41 #: client/src/inventory-scripts/inventory-scripts.form.js:32 #: client/src/inventory-scripts/inventory-scripts.list.js:25 #: client/src/lists/Credentials.js:34 @@ -550,6 +567,12 @@ msgstr "" msgid "Details" msgstr "" +#: client/src/configuration/auth-form/configuration-auth.controller.js:70 +#: client/src/configuration/configuration.controller.js:159 +#: client/src/configuration/configuration.controller.js:212 +msgid "Discard changes" +msgstr "" + #: client/src/forms/Teams.js:148 msgid "Dissasociate permission from team" msgstr "" @@ -567,7 +590,7 @@ msgstr "" msgid "Drag and drop your custom inventory script file here or create one in the field to import your custom inventory." msgstr "" -#: client/src/forms/Projects.js:175 +#: client/src/forms/Projects.js:174 msgid "Each time a job runs using this project, perform an update to the local repository prior to starting the job." msgstr "" @@ -576,7 +599,7 @@ msgstr "" #: client/src/lists/Credentials.js:71 #: client/src/lists/Inventories.js:78 #: client/src/lists/Teams.js:60 -#: client/src/lists/Templates.js:107 +#: client/src/lists/Templates.js:108 #: client/src/lists/Users.js:68 #: client/src/notifications/notificationTemplates.list.js:63 #: client/src/notifications/notificationTemplates.list.js:72 @@ -584,8 +607,8 @@ msgid "Edit" msgstr "" #: client/src/forms/JobTemplates.js:466 -#: client/src/forms/Workflows.js:178 -#: client/src/shared/form-generator.js:1707 +#: client/src/forms/Workflows.js:179 +#: client/src/shared/form-generator.js:1711 msgid "Edit Survey" msgstr "" @@ -613,7 +636,7 @@ msgstr "" msgid "Edit team" msgstr "" -#: client/src/lists/Templates.js:109 +#: client/src/lists/Templates.js:110 msgid "Edit template" msgstr "" @@ -646,7 +669,7 @@ msgstr "" msgid "Enables creation of a provisioning callback URL. Using the URL a host can contact Tower and request a configuration update using this job template." msgstr "" -#: client/src/helpers/Credentials.js:308 +#: client/src/helpers/Credentials.js:306 msgid "Encrypted credentials are not supported." msgstr "" @@ -670,6 +693,10 @@ msgstr "" msgid "Enter the hostname or IP address which corresponds to your VMware vCenter." msgstr "" +#: client/src/configuration/configuration.controller.js:272 +#: client/src/configuration/configuration.controller.js:350 +#: client/src/configuration/configuration.controller.js:384 +#: client/src/configuration/configuration.controller.js:403 #: client/src/controllers/Projects.js:133 #: client/src/controllers/Projects.js:155 #: client/src/controllers/Projects.js:180 @@ -684,8 +711,8 @@ msgstr "" #: client/src/controllers/Users.js:267 #: client/src/controllers/Users.js:321 #: client/src/controllers/Users.js:94 -#: client/src/helpers/Credentials.js:312 -#: client/src/helpers/Credentials.js:328 +#: client/src/helpers/Credentials.js:310 +#: client/src/helpers/Credentials.js:326 #: client/src/login/loginModal/thirdPartySignOn/thirdPartySignOn.service.js:119 msgid "Error!" msgstr "" @@ -711,8 +738,8 @@ msgstr "" #: client/src/forms/JobTemplates.js:352 #: client/src/forms/JobTemplates.js:364 -#: client/src/forms/Workflows.js:71 -#: client/src/forms/Workflows.js:83 +#: client/src/forms/Workflows.js:72 +#: client/src/forms/Workflows.js:84 msgid "Extra Variables" msgstr "" @@ -732,7 +759,7 @@ msgstr "" msgid "Failed to add new user. POST returned status:" msgstr "" -#: client/src/helpers/Credentials.js:313 +#: client/src/helpers/Credentials.js:311 msgid "Failed to create new Credential. POST status:" msgstr "" @@ -753,7 +780,15 @@ msgstr "" msgid "Failed to retrieve user: %s. GET status:" msgstr "" -#: client/src/helpers/Credentials.js:329 +#: client/src/configuration/configuration.controller.js:351 +msgid "Failed to save settings. Returned status:" +msgstr "" + +#: client/src/configuration/configuration.controller.js:385 +msgid "Failed to save toggle settings. Returned status:" +msgstr "" + +#: client/src/helpers/Credentials.js:327 msgid "Failed to update Credential. PUT status:" msgstr "" @@ -802,6 +837,22 @@ msgstr "" msgid "Forks" msgstr "" +#: client/src/configuration/auth-form/configuration-auth.controller.js:102 +msgid "Github" +msgstr "" + +#: client/src/configuration/auth-form/configuration-auth.controller.js:103 +msgid "Github Org" +msgstr "" + +#: client/src/configuration/auth-form/configuration-auth.controller.js:104 +msgid "Github Team" +msgstr "" + +#: client/src/configuration/auth-form/configuration-auth.controller.js:105 +msgid "Google OAuth2" +msgstr "" + #: client/src/forms/Teams.js:118 msgid "Granted Permissions" msgstr "" @@ -949,7 +1000,7 @@ msgstr "" msgid "JOB TEMPLATE" msgstr "" -#: client/src/app.js:425 +#: client/src/app.js:429 #: client/src/dashboard/graphs/job-status/job-status-graph.directive.js:113 #: client/src/main-menu/main-menu.partial.html:122 #: client/src/main-menu/main-menu.partial.html:43 @@ -963,7 +1014,7 @@ msgstr "" msgid "Job Tags" msgstr "" -#: client/src/lists/Templates.js:64 +#: client/src/lists/Templates.js:65 msgid "Job Template" msgstr "" @@ -986,6 +1037,10 @@ msgstr "" msgid "Jobs" msgstr "" +#: client/src/configuration/auth-form/configuration-auth.controller.js:106 +msgid "LDAP" +msgstr "" + #: client/src/main-menu/main-menu.partial.html:83 msgid "LOG OUT" msgstr "" @@ -996,8 +1051,8 @@ msgstr "" #: client/src/forms/JobTemplates.js:340 #: client/src/forms/JobTemplates.js:345 -#: client/src/forms/Workflows.js:59 -#: client/src/forms/Workflows.js:64 +#: client/src/forms/Workflows.js:60 +#: client/src/forms/Workflows.js:65 #: client/src/lists/Templates.js:47 msgid "Labels" msgstr "" @@ -1012,8 +1067,8 @@ msgid "Last Updated" msgstr "" #: client/src/lists/PortalJobTemplates.js:39 -#: client/src/lists/Templates.js:83 -#: client/src/shared/form-generator.js:1699 +#: client/src/lists/Templates.js:84 +#: client/src/shared/form-generator.js:1703 msgid "Launch" msgstr "" @@ -1049,19 +1104,19 @@ msgstr "" msgid "Limit" msgstr "" -#: client/src/shared/socket/socket.service.js:176 +#: client/src/shared/socket/socket.service.js:170 msgid "Live events: attempting to connect to the Tower server." msgstr "" -#: client/src/shared/socket/socket.service.js:180 +#: client/src/shared/socket/socket.service.js:174 msgid "Live events: connected. Pages containing job status information will automatically update in real-time." msgstr "" -#: client/src/shared/socket/socket.service.js:184 +#: client/src/shared/socket/socket.service.js:178 msgid "Live events: error connecting to the Tower server." msgstr "" -#: client/src/shared/form-generator.js:1962 +#: client/src/shared/form-generator.js:1977 msgid "Loading..." msgstr "" @@ -1131,7 +1186,7 @@ msgstr "" #: client/src/forms/Users.js:139 #: client/src/forms/Users.js:164 #: client/src/forms/Users.js:190 -#: client/src/forms/Workflows.js:33 +#: client/src/forms/Workflows.js:34 #: client/src/inventory-scripts/inventory-scripts.form.js:25 #: client/src/inventory-scripts/inventory-scripts.list.js:20 #: client/src/lists/CompletedJobs.js:43 @@ -1195,7 +1250,7 @@ msgid "New User" msgstr "" #: client/src/forms/Workflows.js:19 -msgid "New Workflow" +msgid "New Workflow Job Template" msgstr "" #: client/src/controllers/Users.js:174 @@ -1293,7 +1348,7 @@ msgid "OpenStack domains define administrative boundaries. It is only needed for msgstr "" #: client/src/forms/JobTemplates.js:347 -#: client/src/forms/Workflows.js:66 +#: client/src/forms/Workflows.js:67 msgid "Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs in the Tower display." msgstr "" @@ -1309,8 +1364,8 @@ msgstr "" #: client/src/forms/Projects.js:49 #: client/src/forms/Teams.js:39 #: client/src/forms/Users.js:59 -#: client/src/forms/Workflows.js:46 -#: client/src/forms/Workflows.js:52 +#: client/src/forms/Workflows.js:47 +#: client/src/forms/Workflows.js:53 #: client/src/inventory-scripts/inventory-scripts.form.js:37 #: client/src/inventory-scripts/inventory-scripts.list.js:30 #: client/src/lists/Inventories.js:52 @@ -1337,7 +1392,7 @@ msgid "PASSWORD" msgstr "" #: client/src/organizations/list/organizations-list.partial.html:44 -#: client/src/shared/form-generator.js:1865 +#: client/src/shared/form-generator.js:1880 #: client/src/shared/list-generator/list-generator.factory.js:245 msgid "PLEASE ADD ITEMS TO THIS LIST" msgstr "" @@ -1356,7 +1411,7 @@ msgid "Pagerduty subdomain" msgstr "" #: client/src/forms/JobTemplates.js:358 -#: client/src/forms/Workflows.js:77 +#: client/src/forms/Workflows.js:78 msgid "Pass extra command line variables to the playbook. This is the %s or %s command line parameter for %s. Provide key/value pairs using either YAML or JSON." msgstr "" @@ -1419,8 +1474,8 @@ msgstr "" #: client/src/forms/Inventories.js:142 #: client/src/forms/JobTemplates.js:403 #: client/src/forms/Organizations.js:64 -#: client/src/forms/Projects.js:228 -#: client/src/forms/Workflows.js:115 +#: client/src/forms/Projects.js:227 +#: client/src/forms/Workflows.js:116 msgid "Permissions" msgstr "" @@ -1493,9 +1548,9 @@ msgstr "" #: client/src/forms/Inventories.js:91 #: client/src/forms/JobTemplates.js:396 #: client/src/forms/Organizations.js:57 -#: client/src/forms/Projects.js:220 +#: client/src/forms/Projects.js:219 #: client/src/forms/Teams.js:110 -#: client/src/forms/Workflows.js:108 +#: client/src/forms/Workflows.js:109 msgid "Please save before assigning permissions" msgstr "" @@ -1508,7 +1563,7 @@ msgstr "" msgid "Please save before assigning to teams" msgstr "" -#: client/src/forms/Workflows.js:184 +#: client/src/forms/Workflows.js:185 msgid "Please save before defining the workflow graph" msgstr "" @@ -1593,7 +1648,7 @@ msgstr "" msgid "Project Name" msgstr "" -#: client/src/forms/Projects.js:101 +#: client/src/forms/Projects.js:100 msgid "Project Path" msgstr "" @@ -1638,6 +1693,10 @@ msgstr "" msgid "Provisioning Callback URL" msgstr "" +#: client/src/configuration/auth-form/configuration-auth.controller.js:107 +msgid "RADIUS" +msgstr "" + #: client/src/dashboard/lists/jobs/jobs-list.partial.html:4 msgid "RECENT JOB RUNS" msgstr "" @@ -1684,7 +1743,7 @@ msgstr "" msgid "Remove" msgstr "" -#: client/src/forms/Projects.js:154 +#: client/src/forms/Projects.js:153 msgid "Remove any local modifications prior to performing an update." msgstr "" @@ -1692,6 +1751,20 @@ msgstr "" msgid "Request License" msgstr "" +#: client/src/configuration/auth-form/sub-forms/auth-azure.form.js:41 +#: client/src/configuration/auth-form/sub-forms/auth-github-org.form.js:31 +#: client/src/configuration/auth-form/sub-forms/auth-github-team.form.js:31 +#: client/src/configuration/auth-form/sub-forms/auth-github.form.js:27 +#: client/src/configuration/auth-form/sub-forms/auth-google-oauth2.form.js:39 +#: client/src/configuration/auth-form/sub-forms/auth-ldap.form.js:87 +#: client/src/configuration/auth-form/sub-forms/auth-radius.form.js:32 +#: client/src/configuration/auth-form/sub-forms/auth-saml.form.js:59 +#: client/src/configuration/jobs-form/configuration-jobs.form.js:55 +#: client/src/configuration/system-form/configuration-system.form.js:41 +#: client/src/configuration/ui-form/configuration-ui.form.js:35 +msgid "Reset All" +msgstr "" + #: client/src/lists/Projects.js:42 msgid "Revision" msgstr "" @@ -1704,26 +1777,30 @@ msgstr "" #: client/src/forms/Inventories.js:120 #: client/src/forms/Inventories.js:166 #: client/src/forms/Organizations.js:88 -#: client/src/forms/Projects.js:250 +#: client/src/forms/Projects.js:249 #: client/src/forms/Teams.js:137 #: client/src/forms/Teams.js:99 #: client/src/forms/Users.js:201 msgid "Role" msgstr "" +#: client/src/configuration/auth-form/configuration-auth.controller.js:108 +msgid "SAML" +msgstr "" + #: client/src/controllers/Projects.js:657 msgid "SCM Branch" msgstr "" -#: client/src/forms/Projects.js:155 +#: client/src/forms/Projects.js:154 msgid "SCM Clean" msgstr "" -#: client/src/forms/Projects.js:131 +#: client/src/forms/Projects.js:130 msgid "SCM Credential" msgstr "" -#: client/src/forms/Projects.js:166 +#: client/src/forms/Projects.js:165 msgid "SCM Delete" msgstr "" @@ -1736,7 +1813,7 @@ msgid "SCM Type" msgstr "" #: client/src/dashboard/graphs/dashboard-graphs.partial.html:49 -#: client/src/forms/Projects.js:176 +#: client/src/forms/Projects.js:175 msgid "SCM Update" msgstr "" @@ -1744,7 +1821,7 @@ msgstr "" msgid "SCM Update Cancel" msgstr "" -#: client/src/forms/Projects.js:146 +#: client/src/forms/Projects.js:145 msgid "SCM Update Options" msgstr "" @@ -1765,7 +1842,7 @@ msgstr "" msgid "SIGN IN WITH" msgstr "" -#: client/src/app.js:509 +#: client/src/app.js:513 msgid "SOCKETS" msgstr "" @@ -1798,11 +1875,17 @@ msgstr "" msgid "Save" msgstr "" +#: client/src/configuration/auth-form/configuration-auth.controller.js:81 +#: client/src/configuration/configuration.controller.js:170 +#: client/src/configuration/configuration.controller.js:220 +msgid "Save changes" +msgstr "" + #: client/src/license/license.partial.html:122 msgid "Save successful!" msgstr "" -#: client/src/lists/Templates.js:91 +#: client/src/lists/Templates.js:92 msgid "Schedule" msgstr "" @@ -1814,7 +1897,7 @@ msgstr "" msgid "Schedule future SCM updates" msgstr "" -#: client/src/lists/Templates.js:94 +#: client/src/lists/Templates.js:95 msgid "Schedule future job template runs" msgstr "" @@ -1838,8 +1921,21 @@ msgstr "" msgid "Security Token Service (STS) is a web service that enables you to request temporary, limited-privilege credentials for AWS Identity and Access Management (IAM) users." msgstr "" +#: client/src/shared/form-generator.js:1691 +msgid "Select" +msgstr "" + +#: client/src/configuration/jobs-form/configuration-jobs.controller.js:87 +#: client/src/configuration/ui-form/configuration-ui.controller.js:82 +msgid "Select commands" +msgstr "" + #: client/src/forms/Projects.js:98 -msgid "Select from the list of directories found in the base path.Together the base path and the playbook directory provide the full path used to locate playbooks." +msgid "Select from the list of directories found in the Project Base Path. Together the base path and the playbook directory provide the full path used to locate playbooks." +msgstr "" + +#: client/src/configuration/auth-form/configuration-auth.controller.js:226 +msgid "Select group types" msgstr "" #: client/src/forms/JobTemplates.js:152 @@ -1945,7 +2041,7 @@ msgid "Split up your organization to associate content and control permissions f msgstr "" #: client/src/lists/PortalJobTemplates.js:42 -#: client/src/lists/Templates.js:86 +#: client/src/lists/Templates.js:87 msgid "Start a job using this template" msgstr "" @@ -2017,7 +2113,7 @@ msgstr "" #: client/src/forms/Inventories.js:126 #: client/src/forms/Inventories.js:173 #: client/src/forms/Organizations.js:95 -#: client/src/forms/Projects.js:256 +#: client/src/forms/Projects.js:255 msgid "Team Roles" msgstr "" @@ -2093,6 +2189,14 @@ msgstr "" msgid "There is no SCM update information available for this project. An update has not yet been completed. If you have not already done so, start an update for this project." msgstr "" +#: client/src/configuration/configuration.controller.js:273 +msgid "There was an error resetting value. Returned status:" +msgstr "" + +#: client/src/configuration/configuration.controller.js:404 +msgid "There was an error resetting values. Returned status:" +msgstr "" + #: client/src/helpers/Credentials.js:138 msgid "This is the tenant name. This value is usually the same as the username." msgstr "" @@ -2114,6 +2218,10 @@ msgstr "" msgid "This value does not match the password you entered previously. Please confirm that password." msgstr "" +#: client/src/configuration/configuration.controller.js:429 +msgid "This will reset all configuration values to their factory defaults. Are you sure you want to proceed?" +msgstr "" + #: client/src/dashboard/lists/jobs/jobs-list.partial.html:14 msgid "Time" msgstr "" @@ -2122,7 +2230,7 @@ msgstr "" msgid "Time Remaining" msgstr "" -#: client/src/forms/Projects.js:192 +#: client/src/forms/Projects.js:191 msgid "Time in seconds to consider a project to be current. During job runs and callbacks the task system will evaluate the timestamp of the latest project update. If it is older than Cache Timeout, it is not considered current, and a new project update will be performed." msgstr "" @@ -2192,7 +2300,7 @@ msgstr "" msgid "Update in Progress" msgstr "" -#: client/src/forms/Projects.js:173 +#: client/src/forms/Projects.js:172 msgid "Update on Launch" msgstr "" @@ -2200,11 +2308,6 @@ msgstr "" msgid "Upgrade" msgstr "" -#: client/src/forms/Projects.js:100 -#: client/src/forms/Projects.js:82 -msgid "Use %s in your environment settings file to determine the base path value." -msgstr "" - #: client/src/notifications/notificationTemplates.form.js:404 msgid "Use SSL" msgstr "" @@ -2221,7 +2324,7 @@ msgstr "" #: client/src/forms/Inventories.js:115 #: client/src/forms/Inventories.js:161 #: client/src/forms/Organizations.js:83 -#: client/src/forms/Projects.js:245 +#: client/src/forms/Projects.js:244 #: client/src/forms/Teams.js:94 msgid "User" msgstr "" @@ -2291,7 +2394,7 @@ msgstr "" #: client/src/lists/Credentials.js:80 #: client/src/lists/Inventories.js:85 #: client/src/lists/Teams.js:69 -#: client/src/lists/Templates.js:115 +#: client/src/lists/Templates.js:117 #: client/src/lists/Users.js:78 #: client/src/notifications/notificationTemplates.list.js:80 msgid "View" @@ -2306,8 +2409,8 @@ msgid "View JSON examples at %s" msgstr "" #: client/src/forms/JobTemplates.js:450 -#: client/src/forms/Workflows.js:162 -#: client/src/shared/form-generator.js:1711 +#: client/src/forms/Workflows.js:163 +#: client/src/shared/form-generator.js:1715 msgid "View Survey" msgstr "" @@ -2347,7 +2450,7 @@ msgstr "" msgid "View team" msgstr "" -#: client/src/lists/Templates.js:117 +#: client/src/lists/Templates.js:119 msgid "View template" msgstr "" @@ -2363,6 +2466,16 @@ msgstr "" msgid "View user" msgstr "" +#: client/src/forms/Workflows.js:22 +msgid "WORKFLOW" +msgstr "" + +#: client/src/configuration/auth-form/configuration-auth.controller.js:68 +#: client/src/configuration/configuration.controller.js:157 +#: client/src/configuration/configuration.controller.js:210 +msgid "Warning: Unsaved Changes" +msgstr "" + #: client/src/login/loginModal/loginModal.partial.html:17 msgid "Welcome to Ansible Tower!  Please sign in." msgstr "" @@ -2376,12 +2489,12 @@ msgstr "" msgid "When this template is submitted as a job, setting the type to %s will execute the playbook, running tasks on the selected hosts." msgstr "" -#: client/src/forms/Workflows.js:186 -#: client/src/shared/form-generator.js:1715 +#: client/src/forms/Workflows.js:187 +#: client/src/shared/form-generator.js:1719 msgid "Workflow Editor" msgstr "" -#: client/src/lists/Templates.js:69 +#: client/src/lists/Templates.js:70 msgid "Workflow Job Template" msgstr "" @@ -2397,6 +2510,12 @@ msgstr "" msgid "You do not have permission to add a user." msgstr "" +#: client/src/configuration/auth-form/configuration-auth.controller.js:67 +#: client/src/configuration/configuration.controller.js:156 +#: client/src/configuration/configuration.controller.js:209 +msgid "You have unsaved changes. Would you like to proceed without saving?" +msgstr "" + #: client/src/shared/form-generator.js:960 msgid "Your password must be %d characters long." msgstr "" diff --git a/tools/docker-compose/Dockerfile b/tools/docker-compose/Dockerfile index 37898a6854..6d6b930017 100644 --- a/tools/docker-compose/Dockerfile +++ b/tools/docker-compose/Dockerfile @@ -10,7 +10,7 @@ ADD requirements/requirements.txt requirements/requirements_ansible.txt requirem RUN yum -y update && yum -y install curl epel-release RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - RUN yum -y localinstall http://download.postgresql.org/pub/repos/yum/9.4/redhat/rhel-6-x86_64/pgdg-centos94-9.4-3.noarch.rpm -RUN yum -y update && yum -y install openssh-server ansible mg vim tmux git mercurial subversion python-devel python-psycopg2 make postgresql postgresql-devel nginx nodejs python-psutil libxml2-devel libxslt-devel libstdc++.so.6 gcc cyrus-sasl-devel cyrus-sasl openldap-devel libffi-devel zeromq-devel python-pip xmlsec1-devel swig krb5-devel xmlsec1-openssl xmlsec1 xmlsec1-openssl-devel libtool-ltdl-devel rabbitmq-server bubblewrap +RUN yum -y update && yum -y install openssh-server ansible mg vim tmux git mercurial subversion python-devel python-psycopg2 make postgresql postgresql-devel nginx nodejs python-psutil libxml2-devel libxslt-devel libstdc++.so.6 gcc cyrus-sasl-devel cyrus-sasl openldap-devel libffi-devel zeromq-devel python-pip xmlsec1-devel swig krb5-devel xmlsec1-openssl xmlsec1 xmlsec1-openssl-devel libtool-ltdl-devel rabbitmq-server bubblewrap zanata-python-client gettext RUN pip install virtualenv RUN /usr/bin/ssh-keygen -q -t rsa -N "" -f /root/.ssh/id_rsa RUN mkdir -p /etc/tower diff --git a/tools/scripts/manage_translations.py b/tools/scripts/manage_translations.py old mode 100644 new mode 100755 index f5df39e308..02bcc1c7b5 --- a/tools/scripts/manage_translations.py +++ b/tools/scripts/manage_translations.py @@ -194,9 +194,8 @@ if __name__ == "__main__": except OSError as e: if e.errno == os.errno.ENOENT: print(''' - You need zanata-python-client, install it. - 1. Install zanata-python-client, use - $ dnf install zanata-python-client + You need zanata python client, install it. + 1. Install zanta python client 2. Create ~/.config/zanata.ini file: $ vim ~/.config/zanata.ini [servers] From a700364c8d81b5d5d1e82df69f86227302c8fb12 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 14 Dec 2016 16:06:30 -0500 Subject: [PATCH 147/595] fix job status ui unit test --- awx/ui/tests/spec/job-results/job-results.controller-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/tests/spec/job-results/job-results.controller-test.js b/awx/ui/tests/spec/job-results/job-results.controller-test.js index 1def7d4e94..e978e12449 100644 --- a/awx/ui/tests/spec/job-results/job-results.controller-test.js +++ b/awx/ui/tests/spec/job-results/job-results.controller-test.js @@ -391,7 +391,7 @@ describe('Controller: jobResultsController', () => { status: 'finished' }; $rScope.$broadcast('ws-jobs', eventPayload); - expect($scope.job.status).toBe(eventPayload.status); + expect($scope.job_status).toBe(eventPayload.status); }); }); From e8f01c90291d4573b41364704579f07dee1eb993 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 14 Dec 2016 16:12:36 -0500 Subject: [PATCH 148/595] Stringify the MAX_EVENT_RES_DATA setting when sticking it into an environment variable. --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index addbe4c8f2..2f862e61c2 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -809,7 +809,7 @@ class RunJob(BaseTask): env['REST_API_URL'] = settings.INTERNAL_API_URL env['REST_API_TOKEN'] = job.task_auth_token or '' env['TOWER_HOST'] = settings.TOWER_URL_BASE - env['MAX_EVENT_RES'] = settings.MAX_EVENT_RES_DATA + env['MAX_EVENT_RES'] = str(settings.MAX_EVENT_RES_DATA) env['CALLBACK_QUEUE'] = settings.CALLBACK_QUEUE env['CALLBACK_CONNECTION'] = settings.BROKER_URL if getattr(settings, 'JOB_CALLBACK_DEBUG', False): From ffaa43941abca29ea54d667cff1ee4b8c4941738 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 14 Dec 2016 16:24:55 -0500 Subject: [PATCH 149/595] Added UI support for simultaneous job template runs --- awx/ui/client/src/forms/JobTemplates.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/awx/ui/client/src/forms/JobTemplates.js b/awx/ui/client/src/forms/JobTemplates.js index 23be861723..07f7ddf7c2 100644 --- a/awx/ui/client/src/forms/JobTemplates.js +++ b/awx/ui/client/src/forms/JobTemplates.js @@ -307,6 +307,17 @@ export default dataContainer: "body", labelClass: 'stack-inline', ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' + }, { + name: 'allow_simultaneous', + label: i18n._('Enable Concurrent Jobs'), + type: 'checkbox', + column: 2, + awPopOver: "

    " + i18n._("If enabled, simultaneous runs of this job template will be allowed.") + "

    ", + dataPlacement: 'right', + dataTitle: i18n._('Enable Concurrent Jobs'), + dataContainer: "body", + labelClass: 'stack-inline', + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' }] }, callback_url: { From bdfac35f3f3695286bf019d4e0df5a497dafc89f Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 14 Dec 2016 16:26:18 -0500 Subject: [PATCH 150/595] cancel job also cancels project update related to #4225 --- awx/main/models/jobs.py | 9 +++++++++ awx/main/tasks.py | 20 +++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index b7acf21775..b0b98cb955 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -654,6 +654,15 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin): def get_notification_friendly_name(self): return "Job" + ''' + Canceling a job also cancels the implicit project update with launch_type + run. + ''' + def cancel(self): + res = super(Job, self).cancel() + if self.project_update: + self.project_update.cancel() + return res class JobHostSummary(CreatedModifiedModel): ''' diff --git a/awx/main/tasks.py b/awx/main/tasks.py index e0dcae0fca..6e7a1db6c5 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -708,6 +708,11 @@ class BaseTask(Task): stdout_handle.close() except Exception: pass + + instance = self.update_model(pk) + if instance.cancel_flag: + status = 'canceled' + instance = self.update_model(pk, status=status, result_traceback=tb, output_replacements=output_replacements, **extra_update_fields) @@ -1044,17 +1049,18 @@ class RunJob(BaseTask): local_project_sync = job.project.create_project_update(launch_type="sync") local_project_sync.job_type = 'run' local_project_sync.save() + # save the associated project update before calling run() so that a + # cancel() call on the job can cancel the project update + job = self.update_model(job.pk, project_update=local_project_sync) + project_update_task = local_project_sync._get_task_class() try: project_update_task().run(local_project_sync.id) - job.scm_revision = job.project.scm_revision - job.project_update = local_project_sync - job.save() + job = self.update_model(job.pk, scm_revision=project.scm_revision) except Exception: - job.status = 'failed' - job.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % \ - ('project_update', local_project_sync.name, local_project_sync.id) - job.save() + job = self.update_model(job.pk, status='failed', + job_explanation='Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % \ + ('project_update', local_project_sync.name, local_project_sync.id)) raise def post_run_hook(self, job, status, **kwargs): From 9c444e084eebb7cafb30304e65f01c7520c4dd98 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 14 Dec 2016 16:37:44 -0500 Subject: [PATCH 151/595] Handle psutil errors when terminating tasks. --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 2f862e61c2..6d119ce29a 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -618,7 +618,7 @@ class BaseTask(Task): for child_proc in child_procs: os.kill(child_proc.pid, signal.SIGKILL) os.kill(main_proc.pid, signal.SIGKILL) - except TypeError: + except (TypeError, psutil.Error): os.kill(job.pid, signal.SIGKILL) else: os.kill(job.pid, signal.SIGTERM) From d2f267c34c32998609e97ac6d65ca8d178ccfc2f Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 12 Dec 2016 18:21:35 -0800 Subject: [PATCH 152/595] fixing options request for template list --- .../list/templates-list.controller.js | 56 +++++++++---------- .../templates/list/templates-list.route.js | 14 +++++ 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/awx/ui/client/src/templates/list/templates-list.controller.js b/awx/ui/client/src/templates/list/templates-list.controller.js index 93f32807ea..ff75f8f79b 100644 --- a/awx/ui/client/src/templates/list/templates-list.controller.js +++ b/awx/ui/client/src/templates/list/templates-list.controller.js @@ -4,15 +4,16 @@ * All Rights Reserved *************************************************/ -export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', 'Alert', - 'TemplateList', 'Prompt', 'ClearScope', 'ProcessErrors', 'GetBasePath', - 'InitiatePlaybookRun', 'Wait', '$state', '$filter', 'Dataset', 'rbacUiControlService', 'TemplatesService', - 'QuerySet', 'GetChoices', 'TemplateCopyService', +export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', + 'Alert','TemplateList', 'Prompt', 'ClearScope', 'ProcessErrors', + 'GetBasePath', 'InitiatePlaybookRun', 'Wait', '$state', '$filter', + 'Dataset', 'rbacUiControlService', 'TemplatesService','QuerySet', + 'GetChoices', 'TemplateCopyService', 'DataOptions', function( $scope, $rootScope, $location, $stateParams, Rest, Alert, TemplateList, Prompt, ClearScope, ProcessErrors, GetBasePath, InitiatePlaybookRun, Wait, $state, $filter, Dataset, rbacUiControlService, TemplatesService, - qs, GetChoices, TemplateCopyService + qs, GetChoices, TemplateCopyService, DataOptions ) { ClearScope(); @@ -36,38 +37,31 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', 'Al $scope.list = list; $scope[`${list.iterator}_dataset`] = Dataset.data; $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + $scope.options = DataOptions; $rootScope.flashMessage = null; - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('choicesReady', function() { - $scope[list.name].forEach(function(item, item_idx) { - var itm = $scope[list.name][item_idx]; + $scope.$watchCollection('templates', function() { + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; - // Set the item type label - if (list.fields.type) { - $scope.type_choices.every(function(choice) { - if (choice.value === item.type) { - itm.type_label = choice.label; - return false; - } - return true; - }); - } - }); - }); - - GetChoices({ - scope: $scope, - url: GetBasePath('unified_job_templates'), - field: 'type', - variable: 'type_choices', - callback: 'choicesReady' - }); + // Set the item type label + if (list.fields.type) { + $scope.options.type.choices.every(function(choice) { + if (choice[0] === item.type) { + itm.type_label = choice[1]; + return false; + } + return true; + }); + } + }); + } + ); } + + $scope.$on(`ws-jobs`, function () { // @issue - this is no longer quite as ham-fisted but I'd like for someone else to take a peek // calling $state.reload(); here was problematic when launching a job because job launch also diff --git a/awx/ui/client/src/templates/list/templates-list.route.js b/awx/ui/client/src/templates/list/templates-list.route.js index e615e52db1..5a79700bdb 100644 --- a/awx/ui/client/src/templates/list/templates-list.route.js +++ b/awx/ui/client/src/templates/list/templates-list.route.js @@ -46,6 +46,20 @@ export default { let path = GetBasePath(list.basePath) || GetBasePath(list.name); return qs.search(path, $stateParams[`${list.iterator}_search`]); } + ], + DataOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', 'TemplateList', + function(Rest, GetBasePath, $stateParams, $q, list) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + Rest.setUrl(path); + var val = $q.defer(); + Rest.options() + .then(function(data) { + val.resolve(data.data.actions.GET); + }, function(data) { + val.reject(data); + }); + return val.promise; + } ] } }; From 092c2be0e768da42cd17a6ad23c264d8b29a9507 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 14 Dec 2016 16:57:32 -0500 Subject: [PATCH 153/595] Make any settings read-only that have been modified in custom Python config files. --- awx/conf/settings.py | 16 ++++++++++++++++ awx/settings/development.py | 15 +++++++-------- awx/settings/production.py | 15 +++++++-------- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/awx/conf/settings.py b/awx/conf/settings.py index c08b161237..de8c82e1c5 100644 --- a/awx/conf/settings.py +++ b/awx/conf/settings.py @@ -1,6 +1,7 @@ # Python import contextlib import logging +import sys import threading import time @@ -86,6 +87,7 @@ class SettingsWrapper(UserSettingsHolder): self.__dict__['_awx_conf_settings'] = self self.__dict__['_awx_conf_preload_expires'] = None self.__dict__['_awx_conf_preload_lock'] = threading.RLock() + self.__dict__['_awx_conf_init_readonly'] = False def _get_supported_settings(self): return settings_registry.get_registered_settings() @@ -110,6 +112,20 @@ class SettingsWrapper(UserSettingsHolder): return # Otherwise update local preload timeout. self.__dict__['_awx_conf_preload_expires'] = time.time() + SETTING_CACHE_TIMEOUT + # Check for any settings that have been defined in Python files and + # make those read-only to avoid overriding in the database. + if not self._awx_conf_init_readonly and 'migrate_to_database_settings' not in sys.argv: + defaults_snapshot = self._get_default('DEFAULTS_SNAPSHOT') + for key in self._get_writeable_settings(): + init_default = defaults_snapshot.get(key, None) + try: + file_default = self._get_default(key) + except AttributeError: + file_default = None + if file_default != init_default and file_default is not None: + logger.warning('Setting %s has been marked read-only!', key) + settings_registry._registry[key]['read_only'] = True + self.__dict__['_awx_conf_init_readonly'] = True # If local preload timer has expired, check to see if another process # has already preloaded the cache and skip preloading if so. if cache.get('_awx_conf_preload_expires', empty) is not empty: diff --git a/awx/settings/development.py b/awx/settings/development.py index f2d72a1113..ebe81260d1 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -82,14 +82,13 @@ PASSWORD_HASHERS = ( # Configure a default UUID for development only. SYSTEM_UUID = '00000000-0000-0000-0000-000000000000' -# Store a snapshot of default settings at this point (only for migrating from -# file to database settings). -if 'migrate_to_database_settings' in sys.argv: - DEFAULTS_SNAPSHOT = {} - this_module = sys.modules[__name__] - for setting in dir(this_module): - if setting == setting.upper(): - DEFAULTS_SNAPSHOT[setting] = copy.deepcopy(getattr(this_module, setting)) +# Store a snapshot of default settings at this point before loading any +# customizable config files. +DEFAULTS_SNAPSHOT = {} +this_module = sys.modules[__name__] +for setting in dir(this_module): + if setting == setting.upper(): + DEFAULTS_SNAPSHOT[setting] = copy.deepcopy(getattr(this_module, setting)) # If there is an `/etc/tower/settings.py`, include it. # If there is a `/etc/tower/conf.d/*.py`, include them. diff --git a/awx/settings/production.py b/awx/settings/production.py index 103f775d86..f056a4ea31 100644 --- a/awx/settings/production.py +++ b/awx/settings/production.py @@ -57,14 +57,13 @@ LOGGING['handlers']['fact_receiver']['filename'] = '/var/log/tower/fact_receiver LOGGING['handlers']['system_tracking_migrations']['filename'] = '/var/log/tower/tower_system_tracking_migrations.log' LOGGING['handlers']['rbac_migrations']['filename'] = '/var/log/tower/tower_rbac_migrations.log' -# Store a snapshot of default settings at this point (only for migrating from -# file to database settings). -if 'migrate_to_database_settings' in sys.argv: - DEFAULTS_SNAPSHOT = {} - this_module = sys.modules[__name__] - for setting in dir(this_module): - if setting == setting.upper(): - DEFAULTS_SNAPSHOT[setting] = copy.deepcopy(getattr(this_module, setting)) +# Store a snapshot of default settings at this point before loading any +# customizable config files. +DEFAULTS_SNAPSHOT = {} +this_module = sys.modules[__name__] +for setting in dir(this_module): + if setting == setting.upper(): + DEFAULTS_SNAPSHOT[setting] = copy.deepcopy(getattr(this_module, setting)) # Load settings from any .py files in the global conf.d directory specified in # the environment, defaulting to /etc/tower/conf.d/. From 0900484c621809847c2225d52914708074fa7366 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 14 Dec 2016 14:25:08 -0800 Subject: [PATCH 154/595] hiding status icon on right hand side unless fullscreen is true for job results and workflow results, per request of trahman --- awx/ui/client/src/job-results/job-results.partial.html | 1 + awx/ui/client/src/workflow-results/workflow-results.partial.html | 1 + 2 files changed, 2 insertions(+) diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index c7fd2a1e0d..a763756769 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -414,6 +414,7 @@
    diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index 998a37c956..ff2cd94a0d 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -183,6 +183,7 @@
    From 976e9bfe82a51ee60c4a92ac036a19a87bf99f96 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 14 Dec 2016 14:58:40 -0800 Subject: [PATCH 155/595] adding callback for when options are received --- .../shared/smart-search/queryset.service.js | 18 ++++++++++++++---- .../smart-search/smart-search.controller.js | 9 +++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/awx/ui/client/src/shared/smart-search/queryset.service.js b/awx/ui/client/src/shared/smart-search/queryset.service.js index a45f8af74f..359245bed4 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -18,13 +18,23 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear // grab a single model from the cache, if present if (cache.get(path)) { - defer.resolve({[name] : new DjangoSearchModel(name, path, cache.get(path), relations)}); + defer.resolve({ + models: { + [name] : new DjangoSearchModel(name, path, cache.get(path), relations) + }, + options: cache.get(path) + }); } else { this.url = path; resolve = this.options(path) .then((res) => { base = res.data.actions.GET; - defer.resolve({[name]: new DjangoSearchModel(name, path, base, relations)}); + defer.resolve({ + models: { + [name]: new DjangoSearchModel(name, path, base, relations) + }, + options: res + }); }); } return defer.promise; @@ -76,9 +86,9 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear if (Array.isArray(value)){ return _.map(value, (item) => { return `${key.split('__').join(':')}:${item}`; - }); + }); } - else { + else { return `${key.split('__').join(':')}:${value}`; } }, diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index 87dc33198a..3352cb5495 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -15,8 +15,9 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' path = GetBasePath($scope.basePath) || $scope.basePath; relations = getRelationshipFields($scope.dataset.results); $scope.searchTags = stripDefaultParams($state.params[`${$scope.iterator}_search`]); - qs.initFieldset(path, $scope.djangoModel, relations).then((models) => { - $scope.models = models; + qs.initFieldset(path, $scope.djangoModel, relations).then((data) => { + $scope.models = data.models; + $scope.$emit(`${$scope.list.iterator}_options`, data.options); }); } @@ -102,12 +103,12 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' params.page = '1'; queryset = _.merge(queryset, params, (objectValue, sourceValue, key, object) => { if (object[key] && object[key] !== sourceValue){ - return [object[key], sourceValue]; + return [object[key], sourceValue]; } else { // // https://lodash.com/docs/3.10.1#merge // If customizer fn returns undefined merging is handled by default _.merge algorithm - return undefined; + return undefined; } }); // https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic From c4ce68b4de8c1393fbade3ff191f14141ffe1686 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 14 Dec 2016 14:59:05 -0800 Subject: [PATCH 156/595] applying callback mechanism to job tmeplate list --- .../list/templates-list.controller.js | 52 +++++++++++-------- .../templates/list/templates-list.route.js | 14 ----- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/awx/ui/client/src/templates/list/templates-list.controller.js b/awx/ui/client/src/templates/list/templates-list.controller.js index ff75f8f79b..51b353a51a 100644 --- a/awx/ui/client/src/templates/list/templates-list.controller.js +++ b/awx/ui/client/src/templates/list/templates-list.controller.js @@ -8,12 +8,12 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', 'Alert','TemplateList', 'Prompt', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'InitiatePlaybookRun', 'Wait', '$state', '$filter', 'Dataset', 'rbacUiControlService', 'TemplatesService','QuerySet', - 'GetChoices', 'TemplateCopyService', 'DataOptions', + 'GetChoices', 'TemplateCopyService', function( $scope, $rootScope, $location, $stateParams, Rest, Alert, TemplateList, Prompt, ClearScope, ProcessErrors, GetBasePath, InitiatePlaybookRun, Wait, $state, $filter, Dataset, rbacUiControlService, TemplatesService, - qs, GetChoices, TemplateCopyService, DataOptions + qs, GetChoices, TemplateCopyService ) { ClearScope(); @@ -37,29 +37,39 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', $scope.list = list; $scope[`${list.iterator}_dataset`] = Dataset.data; $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - $scope.options = DataOptions; + $scope.options = {}; $rootScope.flashMessage = null; - - $scope.$watchCollection('templates', function() { - $scope[list.name].forEach(function(item, item_idx) { - var itm = $scope[list.name][item_idx]; - - // Set the item type label - if (list.fields.type) { - $scope.options.type.choices.every(function(choice) { - if (choice[0] === item.type) { - itm.type_label = choice[1]; - return false; - } - return true; - }); - } - }); - } - ); } + $scope.$on(`${list.iterator}_options`, function(event, data){ + debugger; + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); + }); + + $scope.$watchCollection('templates', function() { + optionsRequestDataProcessing(); + } + ); + // iterate over the list and add fields like type label, after the + // OPTIONS request returns, or the list is sorted/paginated/searched + function optionsRequestDataProcessing(){ + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; + + // Set the item type label + if (list.fields.type && $scope.options.hasOwnProperty('type')) { + $scope.options.type.choices.every(function(choice) { + if (choice[0] === item.type) { + itm.type_label = choice[1]; + return false; + } + return true; + }); + } + }); + } $scope.$on(`ws-jobs`, function () { diff --git a/awx/ui/client/src/templates/list/templates-list.route.js b/awx/ui/client/src/templates/list/templates-list.route.js index 5a79700bdb..e615e52db1 100644 --- a/awx/ui/client/src/templates/list/templates-list.route.js +++ b/awx/ui/client/src/templates/list/templates-list.route.js @@ -46,20 +46,6 @@ export default { let path = GetBasePath(list.basePath) || GetBasePath(list.name); return qs.search(path, $stateParams[`${list.iterator}_search`]); } - ], - DataOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', 'TemplateList', - function(Rest, GetBasePath, $stateParams, $q, list) { - let path = GetBasePath(list.basePath) || GetBasePath(list.name); - Rest.setUrl(path); - var val = $q.defer(); - Rest.options() - .then(function(data) { - val.resolve(data.data.actions.GET); - }, function(data) { - val.reject(data); - }); - return val.promise; - } ] } }; From 9e3b5abc37623711aab6bad99f1bd73cd3cb7f2e Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 14 Dec 2016 17:05:15 -0800 Subject: [PATCH 157/595] fixing post-processing for projects list --- awx/ui/client/src/controllers/Projects.js | 38 +++++++++++++++++-- awx/ui/client/src/lists/Projects.js | 1 + .../list/templates-list.controller.js | 1 - 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 17076a2a73..1eb8fbb2df 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -37,11 +37,43 @@ export function ProjectsList($scope, $rootScope, $location, $log, $stateParams, _.forEach($scope[list.name], buildTooltips); $rootScope.flashMessage = null; } - - $scope.$watch(`${list.name}`, function() { - _.forEach($scope[list.name], buildTooltips); + + $scope.$on(`${list.iterator}_options`, function(event, data){ + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); }); + $scope.$watchCollection(`${$scope.list.name}`, function() { + optionsRequestDataProcessing(); + } + ); + + // iterate over the list and add fields like type label, after the + // OPTIONS request returns, or the list is sorted/paginated/searched + function optionsRequestDataProcessing(){ + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; + + // Set the item type label + if (list.fields.scm_type && $scope.options && + $scope.options.hasOwnProperty('scm_type')) { + $scope.options.scm_type.choices.every(function(choice) { + if (choice[0] === item.scm_type) { + itm.type_label = choice[1]; + return false; + } + return true; + }); + } + + _.forEach($scope[list.name], buildTooltips); + + }); + } + // $scope.$watch(`${list.name}`, function() { + // _.forEach($scope[list.name], buildTooltips); + // }); + function buildTooltips(project) { project.statusIcon = GetProjectIcon(project.status); project.statusTip = GetProjectToolTip(project.status); diff --git a/awx/ui/client/src/lists/Projects.js b/awx/ui/client/src/lists/Projects.js index 83cb2eb7e7..5115b0d662 100644 --- a/awx/ui/client/src/lists/Projects.js +++ b/awx/ui/client/src/lists/Projects.js @@ -46,6 +46,7 @@ export default }, scm_type: { label: i18n._('Type'), + ngBind: 'project.type_label', excludeModal: true, columnClass: 'col-lg-3 col-md-2 col-sm-3 hidden-xs' }, diff --git a/awx/ui/client/src/templates/list/templates-list.controller.js b/awx/ui/client/src/templates/list/templates-list.controller.js index 51b353a51a..5bcd659a38 100644 --- a/awx/ui/client/src/templates/list/templates-list.controller.js +++ b/awx/ui/client/src/templates/list/templates-list.controller.js @@ -43,7 +43,6 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', } $scope.$on(`${list.iterator}_options`, function(event, data){ - debugger; $scope.options = data.data.actions.GET; optionsRequestDataProcessing(); }); From db4bb50976cff6a60e292e2a4f32fee8b6e3ca8d Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 14 Dec 2016 17:24:21 -0800 Subject: [PATCH 158/595] adding post-processing for jobs list --- awx/ui/client/src/controllers/Jobs.js | 83 +++++++++++++++-------- awx/ui/client/src/controllers/Projects.js | 7 +- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/awx/ui/client/src/controllers/Jobs.js b/awx/ui/client/src/controllers/Jobs.js index c68c6c5826..3ea53d3191 100644 --- a/awx/ui/client/src/controllers/Jobs.js +++ b/awx/ui/client/src/controllers/Jobs.js @@ -13,7 +13,7 @@ export function JobsListController($state, $rootScope, $log, $scope, $compile, $stateParams, - ClearScope, Find, DeleteJob, RelaunchJob, AllJobsList, ScheduledJobsList, GetBasePath, Dataset, GetChoices) { + ClearScope, Find, DeleteJob, RelaunchJob, AllJobsList, ScheduledJobsList, GetBasePath, Dataset) { ClearScope(); @@ -29,39 +29,68 @@ export function JobsListController($state, $rootScope, $log, $scope, $compile, $ $scope.showJobType = true; - _.forEach($scope[list.name], buildTooltips); - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); + // _.forEach($scope[list.name], buildTooltips); + // if ($scope.removeChoicesReady) { + // $scope.removeChoicesReady(); + // } + // $scope.removeChoicesReady = $scope.$on('choicesReady', function() { + // $scope[list.name].forEach(function(item, item_idx) { + // var itm = $scope[list.name][item_idx]; + // if(item.summary_fields && item.summary_fields.source_workflow_job && + // item.summary_fields.source_workflow_job.id){ + // item.workflow_result_link = `/#/workflows/${item.summary_fields.source_workflow_job.id}`; + // } + // // Set the item type label + // if (list.fields.type) { + // $scope.type_choices.every(function(choice) { + // if (choice.value === item.type) { + // itm.type_label = choice.label; + // return false; + // } + // return true; + // }); + // } + // }); + // }); + + + } + $scope.$on(`${list.iterator}_options`, function(event, data){ + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); + }); + + $scope.$watchCollection(`${$scope.list.name}`, function() { + optionsRequestDataProcessing(); } - $scope.removeChoicesReady = $scope.$on('choicesReady', function() { - $scope[list.name].forEach(function(item, item_idx) { - var itm = $scope[list.name][item_idx]; - if(item.summary_fields && item.summary_fields.source_workflow_job && - item.summary_fields.source_workflow_job.id){ - item.workflow_result_link = `/#/workflows/${item.summary_fields.source_workflow_job.id}`; - } - // Set the item type label - if (list.fields.type) { - $scope.type_choices.every(function(choice) { - if (choice.value === item.type) { - itm.type_label = choice.label; + ); + + // iterate over the list and add fields like type label, after the + // OPTIONS request returns, or the list is sorted/paginated/searched + function optionsRequestDataProcessing(){ + + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; + + if(item.summary_fields && item.summary_fields.source_workflow_job && + item.summary_fields.source_workflow_job.id){ + item.workflow_result_link = `/#/workflows/${item.summary_fields.source_workflow_job.id}`; + } + + // Set the item type label + if (list.fields.type && $scope.options && + $scope.options.hasOwnProperty('type')) { + $scope.options.type.choices.every(function(choice) { + if (choice[0] === item.type) { + itm.type_label = choice[1]; return false; } return true; }); } - }); - }); - - GetChoices({ - scope: $scope, - url: GetBasePath('unified_jobs'), - field: 'type', - variable: 'type_choices', - callback: 'choicesReady' + buildTooltips(itm); }); } - function buildTooltips(job) { job.status_tip = 'Job ' + job.status + ". Click for details."; } @@ -131,5 +160,5 @@ export function JobsListController($state, $rootScope, $log, $scope, $compile, $ } JobsListController.$inject = ['$state', '$rootScope', '$log', '$scope', '$compile', '$stateParams', - 'ClearScope', 'Find', 'DeleteJob', 'RelaunchJob', 'AllJobsList', 'ScheduledJobsList', 'GetBasePath', 'Dataset', 'GetChoices' + 'ClearScope', 'Find', 'DeleteJob', 'RelaunchJob', 'AllJobsList', 'ScheduledJobsList', 'GetBasePath', 'Dataset' ]; diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 1eb8fbb2df..d0c845a058 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -37,7 +37,7 @@ export function ProjectsList($scope, $rootScope, $location, $log, $stateParams, _.forEach($scope[list.name], buildTooltips); $rootScope.flashMessage = null; } - + $scope.$on(`${list.iterator}_options`, function(event, data){ $scope.options = data.data.actions.GET; optionsRequestDataProcessing(); @@ -66,13 +66,10 @@ export function ProjectsList($scope, $rootScope, $location, $log, $stateParams, }); } - _.forEach($scope[list.name], buildTooltips); + buildTooltips(itm); }); } - // $scope.$watch(`${list.name}`, function() { - // _.forEach($scope[list.name], buildTooltips); - // }); function buildTooltips(project) { project.statusIcon = GetProjectIcon(project.status); From 8c55eaec99dde23c9e8ee66f377d47276edf04d5 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 14 Dec 2016 17:38:09 -0800 Subject: [PATCH 159/595] post processing for credentials list --- awx/ui/client/src/controllers/Credentials.js | 30 ++++++++++++++++++++ awx/ui/client/src/controllers/Jobs.js | 27 +----------------- awx/ui/client/src/lists/Credentials.js | 1 + 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/awx/ui/client/src/controllers/Credentials.js b/awx/ui/client/src/controllers/Credentials.js index 1681205992..8ccc5afd78 100644 --- a/awx/ui/client/src/controllers/Credentials.js +++ b/awx/ui/client/src/controllers/Credentials.js @@ -36,6 +36,36 @@ export function CredentialsList($scope, $rootScope, $location, $log, $scope.selected = []; } + $scope.$on(`${list.iterator}_options`, function(event, data){ + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); + }); + + $scope.$watchCollection(`${$scope.list.name}`, function() { + optionsRequestDataProcessing(); + } + ); + // iterate over the list and add fields like type label, after the + // OPTIONS request returns, or the list is sorted/paginated/searched + function optionsRequestDataProcessing(){ + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; + + // Set the item type label + debugger; + if (list.fields.kind && $scope.options && + $scope.options.hasOwnProperty('kind')) { + $scope.options.kind.choices.every(function(choice) { + if (choice[0] === item.kind) { + itm.kind_label = choice[1]; + return false; + } + return true; + }); + } + }); + } + $scope.addCredential = function() { $state.go('credentials.add'); }; diff --git a/awx/ui/client/src/controllers/Jobs.js b/awx/ui/client/src/controllers/Jobs.js index 3ea53d3191..cbf588200c 100644 --- a/awx/ui/client/src/controllers/Jobs.js +++ b/awx/ui/client/src/controllers/Jobs.js @@ -28,33 +28,8 @@ export function JobsListController($state, $rootScope, $log, $scope, $compile, $ $scope[list.name] = $scope[`${list.iterator}_dataset`].results; $scope.showJobType = true; - - // _.forEach($scope[list.name], buildTooltips); - // if ($scope.removeChoicesReady) { - // $scope.removeChoicesReady(); - // } - // $scope.removeChoicesReady = $scope.$on('choicesReady', function() { - // $scope[list.name].forEach(function(item, item_idx) { - // var itm = $scope[list.name][item_idx]; - // if(item.summary_fields && item.summary_fields.source_workflow_job && - // item.summary_fields.source_workflow_job.id){ - // item.workflow_result_link = `/#/workflows/${item.summary_fields.source_workflow_job.id}`; - // } - // // Set the item type label - // if (list.fields.type) { - // $scope.type_choices.every(function(choice) { - // if (choice.value === item.type) { - // itm.type_label = choice.label; - // return false; - // } - // return true; - // }); - // } - // }); - // }); - - } + $scope.$on(`${list.iterator}_options`, function(event, data){ $scope.options = data.data.actions.GET; optionsRequestDataProcessing(); diff --git a/awx/ui/client/src/lists/Credentials.js b/awx/ui/client/src/lists/Credentials.js index d099f2826e..c6e8d6bbae 100644 --- a/awx/ui/client/src/lists/Credentials.js +++ b/awx/ui/client/src/lists/Credentials.js @@ -37,6 +37,7 @@ export default }, kind: { label: i18n._('Type'), + ngBind: 'credential.kind_label', excludeModal: true, nosort: true, columnClass: 'col-md-2 hidden-sm hidden-xs' From 7d90a5205f27787d8f6a99d449d0c9255370aeff Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 14 Dec 2016 20:12:04 -0800 Subject: [PATCH 160/595] removing debugger statement --- awx/ui/client/src/controllers/Credentials.js | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/ui/client/src/controllers/Credentials.js b/awx/ui/client/src/controllers/Credentials.js index 8ccc5afd78..a64056ccd4 100644 --- a/awx/ui/client/src/controllers/Credentials.js +++ b/awx/ui/client/src/controllers/Credentials.js @@ -52,7 +52,6 @@ export function CredentialsList($scope, $rootScope, $location, $log, var itm = $scope[list.name][item_idx]; // Set the item type label - debugger; if (list.fields.kind && $scope.options && $scope.options.hasOwnProperty('kind')) { $scope.options.kind.choices.every(function(choice) { From 3ee8c4159aac0222e264e612177c8202e6e8783e Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 15 Dec 2016 00:16:40 -0500 Subject: [PATCH 161/595] fix tooltip message for notification template (#4448) --- awx/ui/client/src/notifications/notificationTemplates.list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/notifications/notificationTemplates.list.js b/awx/ui/client/src/notifications/notificationTemplates.list.js index f0e1d527d5..eb1d0bbfa7 100644 --- a/awx/ui/client/src/notifications/notificationTemplates.list.js +++ b/awx/ui/client/src/notifications/notificationTemplates.list.js @@ -47,7 +47,7 @@ export default ['i18n', function(i18n){ add: { mode: 'all', // One of: edit, select, all ngClick: 'addNotification()', - awToolTip: i18n._('Create a new custom inventory'), + awToolTip: i18n._('Create a new notification template'), actionClass: 'btn List-buttonSubmit', buttonContent: '+ ' + i18n._('ADD'), ngShow: 'canAdd' From 817a57267462802ea38bf51b3dbe118df0a7c09d Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 14 Dec 2016 22:00:20 -0800 Subject: [PATCH 162/595] adding post processing for notification template list --- .../list.controller.js | 54 ++++++++++--------- .../notificationTemplates.list.js | 1 + 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js index e82cc8219e..208c4a3b8d 100644 --- a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js +++ b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js @@ -29,34 +29,38 @@ $scope.list = list; $scope[`${list.iterator}_dataset`] = Dataset.data; $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'notification_type', - variable: 'notification_type_options', - callback: 'choicesReadyNotifierList' - }); } - $scope.removeChoicesHere = $scope.$on('choicesReadyNotifierList', function() { - list.fields.notification_type.searchOptions = $scope.notification_type_options; - - if ($rootScope.addedItem) { - $scope.addedItem = $rootScope.addedItem; - delete $rootScope.addedItem; - } - $scope.notification_templates.forEach(function(notification_template, i) { - setStatus(notification_template); - $scope.notification_type_options.forEach(function(type) { - if (type.value === notification_template.notification_type) { - $scope.notification_templates[i].notification_type = type.label; - var recent_notifications = notification_template.summary_fields.recent_notifications; - $scope.notification_templates[i].status = recent_notifications && recent_notifications.length > 0 ? recent_notifications[0].status : "none"; - } - }); + $scope.$on(`notification_template_options`, function(event, data){ + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); }); - }); + + $scope.$watchCollection("notification_templates", function() { + optionsRequestDataProcessing(); + } + ); + // iterate over the list and add fields like type label, after the + // OPTIONS request returns, or the list is sorted/paginated/searched. + function optionsRequestDataProcessing(){ + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; + // Set the item type label + if (list.fields.notification_type && $scope.options && + $scope.options.hasOwnProperty('notification_type')) { + $scope.options.notification_type.choices.every(function(choice) { + if (choice[0] === item.notification_type) { + itm.type_label = choice[1]; + var recent_notifications = itm.summary_fields.recent_notifications; + itm.status = recent_notifications && recent_notifications.length > 0 ? recent_notifications[0].status : "none"; + return false; + } + return true; + }); + } + setStatus(itm); + }); + } function setStatus(notification_template) { var html, recent_notifications = notification_template.summary_fields.recent_notifications; diff --git a/awx/ui/client/src/notifications/notificationTemplates.list.js b/awx/ui/client/src/notifications/notificationTemplates.list.js index f0e1d527d5..0987e5fcab 100644 --- a/awx/ui/client/src/notifications/notificationTemplates.list.js +++ b/awx/ui/client/src/notifications/notificationTemplates.list.js @@ -36,6 +36,7 @@ export default ['i18n', function(i18n){ }, notification_type: { label: i18n._('Type'), + ngBind: "notification_template.type_label", searchType: 'select', searchOptions: [], excludeModal: true, From f026a96d94f8b241b68128b8c907f518ae900ae7 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 15 Dec 2016 08:59:24 -0500 Subject: [PATCH 163/595] flake8 fix --- awx/main/tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 705d2153e2..2ad3343607 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1060,8 +1060,8 @@ class RunJob(BaseTask): job = self.update_model(job.pk, scm_revision=project.scm_revision) except Exception: job = self.update_model(job.pk, status='failed', - job_explanation='Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % \ - ('project_update', local_project_sync.name, local_project_sync.id)) + job_explanation=('Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % + ('project_update', local_project_sync.name, local_project_sync.id))) raise def post_run_hook(self, job, status, **kwargs): From 2ec47737bb0cb1d57c43e39a526b323f4f88ca29 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 15 Dec 2016 09:29:31 -0500 Subject: [PATCH 164/595] another flake8 fix --- awx/main/models/jobs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 374cc002bb..8a6b9fc91d 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -664,6 +664,7 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin): self.project_update.cancel() return res + class JobHostSummary(CreatedModifiedModel): ''' Per-host statistics for each job. From e69f6726d0ed72eaa49cf14751cda6e03d3d4cb8 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 15 Dec 2016 09:31:58 -0500 Subject: [PATCH 165/595] fix unit test --- awx/main/tests/unit/test_network_credential.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tests/unit/test_network_credential.py b/awx/main/tests/unit/test_network_credential.py index 6517c14a89..7ae97fe76b 100644 --- a/awx/main/tests/unit/test_network_credential.py +++ b/awx/main/tests/unit/test_network_credential.py @@ -77,7 +77,7 @@ def test_net_cred_ssh_agent(mocker, get_ssh_version): mocker.patch.object(run_job, 'post_run_hook', return_value=None) run_job.run(mock_job.id) - assert run_job.update_model.call_count == 3 + assert run_job.update_model.call_count == 4 job_args = run_job.update_model.call_args_list[1][1].get('job_args') assert 'ssh-add' in job_args From 69292df170fc0482e2c86b8db9a88ad6c4344442 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 15 Dec 2016 12:05:22 -0500 Subject: [PATCH 166/595] Moved toggle password logic to a directive that can be added as an attribute of the button. --- awx/ui/client/src/shared/directives.js | 18 ++++++++++++++++++ awx/ui/client/src/shared/form-generator.js | 15 +-------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/awx/ui/client/src/shared/directives.js b/awx/ui/client/src/shared/directives.js index 16a0faea9a..3c938d343e 100644 --- a/awx/ui/client/src/shared/directives.js +++ b/awx/ui/client/src/shared/directives.js @@ -1133,4 +1133,22 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper']) }); } }; +}]) + +.directive('awPasswordToggle', [function() { + return { + restrict: 'A', + link: function(scope, element) { + $(element).click(function() { + var buttonInnerHTML = $(element).html(); + if (buttonInnerHTML.indexOf("Show") > -1) { + $(element).html("Hide"); + $(element).closest('.input-group').find('input').first().attr("type", "text"); + } else { + $(element).html("Show"); + $(element).closest('.input-group').find('input').first().attr("type", "password"); + } + }); + } + }; }]); diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index aca1c2857c..d487efa925 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -846,28 +846,15 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "\t" + label(); if (field.hasShowInputButton) { var tooltip = i18n._("Toggle the display of plaintext."); - field.toggleInput = function(id) { - var buttonId = id + "_show_input_button", - inputId = id + "_input", - buttonInnerHTML = $(buttonId).html(); - if (buttonInnerHTML.indexOf("Show") > -1) { - $(buttonId).html("Hide"); - $(inputId).attr("type", "text"); - } else { - $(buttonId).html("Show"); - $(inputId).attr("type", "password"); - } - }; html += "\
    \n"; // TODO: make it so that the button won't show up if the mode is edit, hasShowInputButton !== true, and there are no contents in the field. html += "\n"; - html += "
    -
    +
    Use the inventory root
    diff --git a/awx/ui/client/src/inventories/manage/copy-move/copy-move.route.js b/awx/ui/client/src/inventories/manage/copy-move/copy-move.route.js index 6e2c190e3f..f15705778d 100644 --- a/awx/ui/client/src/inventories/manage/copy-move/copy-move.route.js +++ b/awx/ui/client/src/inventories/manage/copy-move/copy-move.route.js @@ -10,14 +10,31 @@ import CopyMoveHostsController from './copy-move-hosts.controller'; var copyMoveGroupRoute = { name: 'inventoryManage.copyMoveGroup', - url: '/copy-move-group/{group_id}', + url: '/copy-move-group/{group_id:int}', + searchPrefix: 'copy', data: { group_id: 'group_id', }, + params: { + copy_search: { + value: { + not__id__in: null + }, + dynamic: true, + squash: '' + } + }, ncyBreadcrumb: { label: "COPY OR MOVE {{item.name}}" }, resolve: { + Dataset: ['CopyMoveGroupList', 'QuerySet', '$stateParams', 'GetBasePath', 'group', + function(list, qs, $stateParams, GetBasePath, group) { + $stateParams.copy_search.not__id__in = ($stateParams.group.length > 0 ? group.id + ',' + _.last($stateParams.group) : group.id); + let path = GetBasePath(list.name); + return qs.search(path, $stateParams.copy_search); + } + ], group: ['GroupManageService', '$stateParams', function(GroupManageService, $stateParams){ return GroupManageService.get({id: $stateParams.group_id}).then(res => res.data.results[0]); }] @@ -26,16 +43,33 @@ var copyMoveGroupRoute = { 'form@inventoryManage' : { controller: CopyMoveGroupsController, templateUrl: templateUrl('inventories/manage/copy-move/copy-move'), + }, + 'copyMoveList@inventoryManage.copyMoveGroup': { + templateProvider: function(CopyMoveGroupList, generateList) { + let html = generateList.build({ + list: CopyMoveGroupList, + mode: 'lookup', + input_type: 'radio' + }); + return html; + } } } }; var copyMoveHostRoute = { name: 'inventoryManage.copyMoveHost', url: '/copy-move-host/{host_id}', + searchPrefix: 'copy', ncyBreadcrumb: { label: "COPY OR MOVE {{item.name}}" }, resolve: { + Dataset: ['CopyMoveGroupList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.name); + return qs.search(path, $stateParams.copy_search); + } + ], host: ['HostManageService', '$stateParams', function(HostManageService, $stateParams){ return HostManageService.get({id: $stateParams.host_id}).then(res => res.data.results[0]); }] @@ -44,6 +78,16 @@ var copyMoveHostRoute = { 'form@inventoryManage': { templateUrl: templateUrl('inventories/manage/copy-move/copy-move'), controller: CopyMoveHostsController, + }, + 'copyMoveList@inventoryManage.copyMoveHost': { + templateProvider: function(CopyMoveGroupList, generateList) { + let html = generateList.build({ + list: CopyMoveGroupList, + mode: 'lookup', + input_type: 'radio' + }); + return html; + } } } }; diff --git a/awx/ui/client/src/lists/Groups.js b/awx/ui/client/src/lists/Groups.js index 072c35f9a3..0bfc6a9f3f 100644 --- a/awx/ui/client/src/lists/Groups.js +++ b/awx/ui/client/src/lists/Groups.js @@ -12,7 +12,7 @@ export default .value('CopyMoveGroupList', { name: 'groups', - iterator: 'group', + iterator: 'copy', selectTitle: 'Copy Groups', index: false, well: false, diff --git a/awx/ui/client/src/shared/stateExtender.provider.js b/awx/ui/client/src/shared/stateExtender.provider.js index da8b34fbee..f9534c2665 100644 --- a/awx/ui/client/src/shared/stateExtender.provider.js +++ b/awx/ui/client/src/shared/stateExtender.provider.js @@ -31,7 +31,7 @@ export default function($stateProvider) { order_by: "name" }, dynamic: true, - squash: true + squash: '' } } }; From fed98063135246ded43526f4e22f17df80a8b5e2 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Sat, 26 Nov 2016 16:14:19 -0500 Subject: [PATCH 171/595] Reorganize awx/ui/client/src/access module structure addPermissions module => add-rbac-resource module addPermissionsList module => rbac-multiselect-list module roleSelect module => rbac-multiselect-role module roleList module => rbac-role-column module Use isolate scope in rbac-multiselect-role module Move shared styles to module root --- .../src/access/add-rbac-resource/main.js | 12 +++ .../rbac-resource.controller.js} | 16 ++-- .../rbac-resource.directive.js} | 6 +- .../rbac-resource.partial.html} | 10 +-- ...issions.block.less => add-rbac.block.less} | 10 ++- .../addPermissionsList.directive.js | 47 ----------- .../client/src/access/addPermissions/main.js | 14 ---- awx/ui/client/src/access/main.js | 10 ++- .../main.js | 8 +- .../permissionsTeams.list.js | 0 .../permissionsUsers.list.js | 0 .../rbac-multiselect-list.directive.js | 80 +++++++++++++++++++ .../rbac-multiselect-role.directive.js} | 8 +- .../roleList.block.less | 2 +- .../roleList.directive.js | 2 +- .../roleList.partial.html | 0 16 files changed, 133 insertions(+), 92 deletions(-) create mode 100644 awx/ui/client/src/access/add-rbac-resource/main.js rename awx/ui/client/src/access/{addPermissions/addPermissions.controller.js => add-rbac-resource/rbac-resource.controller.js} (91%) rename awx/ui/client/src/access/{addPermissions/addPermissions.directive.js => add-rbac-resource/rbac-resource.directive.js} (82%) rename awx/ui/client/src/access/{addPermissions/addPermissions.partial.html => add-rbac-resource/rbac-resource.partial.html} (91%) rename awx/ui/client/src/access/{addPermissions/addPermissions.block.less => add-rbac.block.less} (95%) delete mode 100644 awx/ui/client/src/access/addPermissions/addPermissionsList/addPermissionsList.directive.js delete mode 100644 awx/ui/client/src/access/addPermissions/main.js rename awx/ui/client/src/access/{addPermissions/addPermissionsList => rbac-multiselect}/main.js (55%) rename awx/ui/client/src/access/{addPermissions/addPermissionsList => rbac-multiselect}/permissionsTeams.list.js (100%) rename awx/ui/client/src/access/{addPermissions/addPermissionsList => rbac-multiselect}/permissionsUsers.list.js (100%) create mode 100644 awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js rename awx/ui/client/src/access/{addPermissions/roleSelect.directive.js => rbac-multiselect/rbac-multiselect-role.directive.js} (67%) rename awx/ui/client/src/access/{ => rbac-role-column}/roleList.block.less (96%) rename awx/ui/client/src/access/{ => rbac-role-column}/roleList.directive.js (95%) rename awx/ui/client/src/access/{ => rbac-role-column}/roleList.partial.html (100%) diff --git a/awx/ui/client/src/access/add-rbac-resource/main.js b/awx/ui/client/src/access/add-rbac-resource/main.js new file mode 100644 index 0000000000..346e6106c6 --- /dev/null +++ b/awx/ui/client/src/access/add-rbac-resource/main.js @@ -0,0 +1,12 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import addRbacResourceDirective from './rbac-resource.directive'; +import rbacMultiselect from '../rbac-multiselect/main'; + +export default + angular.module('AddRbacResourceModule', [rbacMultiselect.name]) + .directive('addRbacResource', addRbacResourceDirective); diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.controller.js b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.controller.js similarity index 91% rename from awx/ui/client/src/access/addPermissions/addPermissions.controller.js rename to awx/ui/client/src/access/add-rbac-resource/rbac-resource.controller.js index d5774e7c79..99cf7e9e11 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.controller.js +++ b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.controller.js @@ -18,16 +18,7 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'Pr // the object permissions are being added to scope.object = scope.resourceData.data; // array for all possible roles for the object - scope.roles = Object - .keys(scope.object.summary_fields.object_roles) - .map(function(key) { - return { - value: scope.object.summary_fields - .object_roles[key].id, - label: scope.object.summary_fields - .object_roles[key].name - }; - }); + scope.roles = scope.object.summary_fields.object_roles; // TODO: get working with api // array w roles and descriptions for key @@ -44,6 +35,11 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'Pr scope.showKeyPane = false; + scope.removeObject = function(obj){ + _.remove(scope.allSelected, {id: obj.id}); + obj.isSelected = false; + }; + scope.toggleKeyPane = function() { scope.showKeyPane = !scope.showKeyPane; }; diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.directive.js b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.directive.js similarity index 82% rename from awx/ui/client/src/access/addPermissions/addPermissions.directive.js rename to awx/ui/client/src/access/add-rbac-resource/rbac-resource.directive.js index 284110b0ce..fde8ee4054 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.directive.js +++ b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.directive.js @@ -3,7 +3,7 @@ * * All Rights Reserved *************************************************/ -import addPermissionsController from './addPermissions.controller'; +import controller from './rbac-resource.controller'; /* jshint unused: vars */ export default ['templateUrl', '$state', @@ -16,8 +16,8 @@ export default ['templateUrl', '$state', teamsDataset: '=', resourceData: '=', }, - controller: addPermissionsController, - templateUrl: templateUrl('access/addPermissions/addPermissions'), + controller: controller, + templateUrl: templateUrl('access/add-rbac-resource/rbac-resource'), link: function(scope, element, attrs) { scope.toggleFormTabs('users'); $('#add-permissions-modal').modal('show'); diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.partial.html b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html similarity index 91% rename from awx/ui/client/src/access/addPermissions/addPermissions.partial.html rename to awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html index 264a4cf834..e8c3361f91 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.partial.html +++ b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html @@ -46,10 +46,10 @@
    - +
    - +
    Please assign roles to the selected users/teams -
    Key @@ -91,8 +91,8 @@ {{ obj.type }}
    - - + +
    \n"; } + html += '
    '; } } diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index b76c5b5a36..62b7f9cb68 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -230,7 +230,74 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat */ generateFormListDefinitions: function(form, formStateDefinition) { - function buildPermissionDirective() { + function buildRbacUserTeamDirective(){ + let states = []; + + states.push($stateExtender.buildDefinition({ + name: `${formStateDefinition.name}.permissions.add`, + squashSearchUrl: true, + url: '/add-permissions', + params: { + project_search: { + value: {order_by: 'name', page_size: '5'}, + dynamic: true + }, + template_search: { + value: {order_by: 'name', page_size: '5', type: 'workflow_job_template,job_template'}, // @issue and also system_job_template? + dynamic: true + }, + inventory_search: { + value: {order_by: 'name', page_size: '5'}, + dynamic: true + }, + credential_search: { + value: {order_by: 'name', page_size: '5'}, + dynamic: true + } + }, + views: { + [`modal@${formStateDefinition.name}`]: { + template: `` + } + }, + resolve: { + templatesDataset: ['TemplateList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + projectsDataset: ['ProjectList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + inventoriesDataset: ['InventoryList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + credentialsDataset: ['CredentialList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + }, + onExit: function($state) { + if ($state.transition) { + $('#add-permissions-modal').modal('hide'); + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open'); + } + }, + })); + return states; + } + + function buildRbacResourceDirective() { let states = []; states.push($stateExtender.buildDefinition({ @@ -249,7 +316,7 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat }, views: { [`modal@${formStateDefinition.name}`]: { - template: `` + template: `` } }, resolve: { @@ -282,7 +349,13 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat let states = []; states.push(buildListDefinition(field)); if (field.iterator === 'permission' && field.actions && field.actions.add) { - states.push(buildPermissionDirective()); + if (form.name === 'user' || form.name === 'team'){ + states.push(buildRbacUserTeamDirective()); + + } + else { + states.push(buildRbacResourceDirective()); + } states = _.flatten(states); } return states; From ce12b97d755e3f6cc874c3a5510a6b328b84cea9 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Sat, 26 Nov 2016 16:12:57 -0500 Subject: [PATCH 174/595] New permission directive - add-rbac-user-team --- .../src/access/add-rbac-user-team/main.js | 12 ++ .../rbac-user-team.controller.js | 117 ++++++++++++ .../rbac-user-team.directive.js | 25 +++ .../rbac-user-team.partial.html | 175 ++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 awx/ui/client/src/access/add-rbac-user-team/main.js create mode 100644 awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js create mode 100644 awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js create mode 100644 awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html diff --git a/awx/ui/client/src/access/add-rbac-user-team/main.js b/awx/ui/client/src/access/add-rbac-user-team/main.js new file mode 100644 index 0000000000..547815a1d7 --- /dev/null +++ b/awx/ui/client/src/access/add-rbac-user-team/main.js @@ -0,0 +1,12 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import addRbacUserTeamDirective from './rbac-user-team.directive'; +import rbacMultiselect from '../rbac-multiselect/main'; + +export default + angular.module('AddRbacUserTeamModule', [rbacMultiselect.name]) + .directive('addRbacUserTeam', addRbacUserTeamDirective); diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js new file mode 100644 index 0000000000..981478d6d0 --- /dev/null +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js @@ -0,0 +1,117 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Access + * @description + * Controller for handling permissions adding + */ + +export default ['$rootScope', '$scope', '$state', 'GetBasePath', 'Rest', '$q', 'Wait', 'ProcessErrors', +function(rootScope, scope, $state, GetBasePath, Rest, $q, Wait, ProcessErrors) { + + init(); + + function init(){ + + let resources = ['templates', 'projects', 'inventories', 'credentials']; + + // data model: + // selected - keyed by type of resource + // selected[type] - keyed by each resource object's id + // selected[type][id] === { roles: [ ... ], ... } + scope.selected = {}; + _.each(resources, (resource) => scope.selected[resource] = {}); + + scope.keys = {}; + _.each(resources, (resource) => scope.keys[resource] = {}); + + scope.tab = { + templates: true, + projects: false, + inventories: false, + credentials: false + }; + scope.showKeyPane = false; + scope.owner = scope.resolve.resourceData.data; + } + + // aggregate name/descriptions for each available role, based on resource type + function aggregateKey(item, type){ + _.merge(scope.keys[type], item.summary_fields.object_roles); + } + + scope.closeModal = function() { + $state.go('^', null, {reload: true}); + }; + + scope.currentTab = function(){ + return _.findKey(scope.tab, (tab) => tab); + }; + + scope.toggleKeyPane = function() { + scope.showKeyPane = !scope.showKeyPane; + }; + + scope.showSection2Container = function(){ + return _.any(scope.selected, (type) => Object.keys(type).length > 0); + }; + + scope.showSection2Tab = function(tab){ + return Object.keys(scope.selected[tab]).length > 0; + }; + + scope.removeSelection = function(resource, type){ + delete scope.selected[type][resource.id]; + resource.isSelected = false; + }; + + // handle form tab changes + scope.selectTab = function(selected){ + _.each(scope.tab, (value, key, collection) => { + collection[key] = (selected === key); + }); + }; + + // pop/push into unified collection of selected users & teams + scope.$on("selectedOrDeselected", function(e, value) { + let resourceType = scope.currentTab(), + item = value.value; + + if (item.isSelected) { + scope.selected[resourceType][item.id] = item; + scope.selected[resourceType][item.id].roles = []; + aggregateKey(item, resourceType); + } else { + delete scope.selected[resourceType][item.id]; + } + }); + + // post roles to api + scope.saveForm = function() { + Wait('start'); + // scope.selected => { n: {id: n}, ... } => [ {id: n}, ... ] + let requests = _(scope.selected).map((type) => { + return _.map(type, (resource) => resource.roles); + }).flattenDeep().value(); + + Rest.setUrl(scope.owner.related.roles); + + $q.all( _.map(requests, (entity) => Rest.post({id: entity.id})) ) + .then( () =>{ + Wait('stop'); + scope.closeModal(); + }, (error) => { + scope.closeModal(); + ProcessErrors(null, error.data, error.status, null, { + hdr: 'Error!', + msg: 'Failed to post role(s): POST returned status' + + error.status + }); + }); + }; +}]; diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js new file mode 100644 index 0000000000..2b89195820 --- /dev/null +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js @@ -0,0 +1,25 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ +import controller from './rbac-user-team.controller'; + +/* jshint unused: vars */ +export default ['templateUrl', + function(templateUrl) { + return { + restrict: 'E', + scope: { + resolve: "=" + }, + controller: controller, + controllerAs: 'rbac', + templateUrl: templateUrl('access/add-rbac-user-team/rbac-user-team'), + link: function(scope, element, attrs) { + $('#add-permissions-modal').modal('show'); + window.scrollTo(0, 0); + } + }; + } +]; diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html new file mode 100644 index 0000000000..e6ba2326af --- /dev/null +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html @@ -0,0 +1,175 @@ + From c753328a0b6f793a964ced36cce79d3e7423b5fb Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Tue, 13 Dec 2016 15:33:47 -0500 Subject: [PATCH 175/595] split templates into job templates / workflow job templates --- .../rbac-user-team.controller.js | 3 +- .../rbac-user-team.directive.js | 1 - .../rbac-user-team.partial.html | 20 ++++-- .../rbac-multiselect-list.directive.js | 62 +++++++++++++------ .../src/shared/stateDefinitions.factory.js | 22 +++++-- 5 files changed, 76 insertions(+), 32 deletions(-) diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js index 981478d6d0..9888370e54 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js @@ -31,7 +31,8 @@ function(rootScope, scope, $state, GetBasePath, Rest, $q, Wait, ProcessErrors) { _.each(resources, (resource) => scope.keys[resource] = {}); scope.tab = { - templates: true, + job_templates: true, + workflow_templates: false, projects: false, inventories: false, credentials: false diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js index 2b89195820..8790410ee3 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js @@ -14,7 +14,6 @@ export default ['templateUrl', resolve: "=" }, controller: controller, - controllerAs: 'rbac', templateUrl: templateUrl('access/add-rbac-user-team/rbac-user-team'), link: function(scope, element, attrs) { $('#add-permissions-modal').modal('show'); diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html index e6ba2326af..f148a25d70 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html @@ -33,14 +33,19 @@
    + ng-click="selectTab('job_templates')" + ng-class="{'is-selected': tab.job_templates }"> Templates
    + Workflow Templates +
    +
    Projects
    -
    - +
    + +
    +
    +
    diff --git a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js index 1cc5f29f3d..d94411ff0f 100644 --- a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js +++ b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js @@ -24,7 +24,8 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL Teams: addPermissionsTeamsList, Users: addPermissionsUsersList, Projects: ProjectList, - Templates: TemplateList, + JobTemplates: TemplateList, + WorkflowTemplates: TemplateList, Inventories: InventoryList, Credentials: CredentialList }; @@ -34,23 +35,48 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL list.listTitleBadge = false; delete list.actions; delete list.fieldActions; - - if (scope.view !== 'Users' && scope.view !== 'Teams' && scope.view !=='Projects' && scope.view !== 'Inventories'){ - list.fields = { - name: list.fields.name, - description: list.fields.description - }; - } else if (scope.view === 'Projects'){ - list.fields = { - name: list.fields.name, - scm_type: list.fields.scm_type - }; - } else if (scope.view === 'Inventories'){ - list.fieds = { - name: list.fields.name, - organization: list.fields.organization - }; - } + + switch(scope.view){ + + case 'Projects': + list.fields = { + name: list.fields.name, + scm_type: list.fields.scm_type + }; + break; + + case 'Inventories': + list.fields = { + name: list.fields.name, + organization: list.fields.organization + }; + break; + + case 'JobTemplates': + list.name = 'job_templates'; + list.iterator = 'job_template'; + list.fields = { + name: list.fields.name, + description: list.fields.description + }; + break; + + case 'WorkflowTemplates': + list.name = 'workflow_templates'; + list.iterator = 'workflow_template', + list.basePath = 'workflow_job_templates'; + list.fields = { + name: list.fields.name, + description: list.fields.description + }; + break; + + default: + list.fields = { + name: list.fields.name, + description: list.fields.description + }; + } list_html = generateList.build({ mode: 'edit', diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index 62b7f9cb68..21ba01aade 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -242,8 +242,12 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat value: {order_by: 'name', page_size: '5'}, dynamic: true }, - template_search: { - value: {order_by: 'name', page_size: '5', type: 'workflow_job_template,job_template'}, // @issue and also system_job_template? + job_template_search: { + value: {order_by: 'name', page_size: '5'}, + dynamic: true + }, + workflow_template_search: { + value: {order_by: 'name', page_size: '5'}, dynamic: true }, inventory_search: { @@ -261,10 +265,16 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat } }, resolve: { - templatesDataset: ['TemplateList', 'QuerySet', '$stateParams', 'GetBasePath', - function(list, qs, $stateParams, GetBasePath) { - let path = GetBasePath(list.basePath) || GetBasePath(list.name); - return qs.search(path, $stateParams[`${list.iterator}_search`]); + jobTemplatesDataset: ['QuerySet', '$stateParams', 'GetBasePath', + function(qs, $stateParams, GetBasePath) { + let path = GetBasePath('job_templates'); + return qs.search(path, $stateParams.job_template_search); + } + ], + workflowTemplatesDataset: ['QuerySet', '$stateParams', 'GetBasePath', + function(qs, $stateParams, GetBasePath) { + let path = GetBasePath('workflow_job_templates'); + return qs.search(path, $stateParams.workflow_template_search); } ], projectsDataset: ['ProjectList', 'QuerySet', '$stateParams', 'GetBasePath', From 1090d74ea03522330f32a97afc92837bcd949308 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Tue, 13 Dec 2016 23:19:50 -0500 Subject: [PATCH 176/595] rough pass on rbac-selected-list directive --- .../src/access/add-rbac-user-team/main.js | 7 +- .../rbac-selected-list.directive.js | 108 ++++++++++++++++++ .../rbac-user-team.controller.js | 2 +- .../rbac-user-team.directive.js | 1 + .../rbac-user-team.partial.html | 53 ++++----- .../list-generator/list-generator.factory.js | 4 +- 6 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js diff --git a/awx/ui/client/src/access/add-rbac-user-team/main.js b/awx/ui/client/src/access/add-rbac-user-team/main.js index 547815a1d7..94a1fa7c60 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/main.js +++ b/awx/ui/client/src/access/add-rbac-user-team/main.js @@ -5,8 +5,9 @@ *************************************************/ import addRbacUserTeamDirective from './rbac-user-team.directive'; -import rbacMultiselect from '../rbac-multiselect/main'; +import rbacSelectedList from './rbac-selected-list.directive'; export default - angular.module('AddRbacUserTeamModule', [rbacMultiselect.name]) - .directive('addRbacUserTeam', addRbacUserTeamDirective); + angular.module('AddRbacUserTeamModule', []) + .directive('addRbacUserTeam', addRbacUserTeamDirective) + .directive('rbacSelectedList', rbacSelectedList); \ No newline at end of file diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js new file mode 100644 index 0000000000..131a060523 --- /dev/null +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js @@ -0,0 +1,108 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/* jshint unused: vars */ +export default ['$compile','templateUrl', 'i18n', 'generateList', + 'ProjectList', 'TemplateList', 'InventoryList', 'CredentialList', + function($compile, templateUrl, i18n, generateList, + ProjectList, TemplateList, InventoryList, CredentialList) { + return { + restrict: 'E', + scope: { + resourceType: "=", + collection: "=", + selected: "=" + }, + link: function(scope, element, attrs) { + console.log(scope.resourceType) + let listMap, list, list_html; + + listMap = { + projects: ProjectList, + job_templates: TemplateList, + workflow_templates: TemplateList, + inventories: InventoryList, + credentials: CredentialList + }; + + list = _.cloneDeep(listMap[scope.resourceType]) + + list.fieldActions = { + remove: { + ngClick: `removeSelection(${list.iterator}, resourceType)`, + icon: 'fa-remove', + awToolTip: i18n._(`Remove ${list.iterator}`), + label: i18n._('Remove'), + class: 'btn-sm' + } + }; + delete list.actions; + + list.listTitleBadge = false; + + switch(scope.resourceType){ + + case 'projects': + list.fields = { + name: list.fields.name, + scm_type: list.fields.scm_type + }; + break; + + case 'inventories': + list.fields = { + name: list.fields.name, + organization: list.fields.organization + }; + break; + + case 'job_templates': + list.name = 'job_templates'; + list.iterator = 'job_template'; + list.fields = { + name: list.fields.name, + description: list.fields.description + }; + break; + + case 'workflow_templates': + list.name = 'workflow_templates'; + list.iterator = 'workflow_template', + list.basePath = 'workflow_job_templates'; + list.fields = { + name: list.fields.name, + description: list.fields.description + }; + break; + + default: + list.fields = { + name: list.fields.name, + description: list.fields.description + }; + } + + list.fields = _.each(list.fields, (field) => field.nosort = true); + + list_html = generateList.build({ + mode: 'edit', + list: list, + related: false, + title: false, + showSearch: false, + paginate: false + }); + + scope.list = list; + scope[`${list.iterator}_dataset`] = scope.collection; + scope[list.name] = scope.collection; + + element.append(list_html); + $compile(element.contents())(scope); + } + }; + } +]; diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js index 9888370e54..3e41460487 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js @@ -18,7 +18,7 @@ function(rootScope, scope, $state, GetBasePath, Rest, $q, Wait, ProcessErrors) { function init(){ - let resources = ['templates', 'projects', 'inventories', 'credentials']; + let resources = ['job_templates', 'workflow_templates', 'projects', 'inventories', 'credentials']; // data model: // selected - keyed by type of resource diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js index 8790410ee3..ced1ceb744 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.directive.js @@ -16,6 +16,7 @@ export default ['templateUrl', controller: controller, templateUrl: templateUrl('access/add-rbac-user-team/rbac-user-team'), link: function(scope, element, attrs) { + scope.selectTab('job_templates'); $('#add-permissions-modal').modal('show'); window.scrollTo(0, 0); } diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html index f148a25d70..dc330bc035 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html @@ -35,7 +35,7 @@
    - Templates + Job Templates
    - Templates + ng-click="selectTab('job_templates')" + ng-class="{'is-selected': tab.job_templates }" + ng-show="showSection2Tab('job_templates')"> + Job Templates +
    +
    + Workflow Templates
    -
    - -
    - -
    -
    - - {{ resource.name }} - - - {{ resource.type }} - -
    - - - -
    -
    -
    + + + + + +
    + + +
    +
    diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index c41e059c81..b1a74db51a 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -443,12 +443,14 @@ export default ['$location', '$compile', '$rootScope', 'Attr', 'Icon', html += "
    \n"; } - html += `
    `; + } return html; }, From 81d152443957d50c392ca06a4643430afb311972 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Wed, 14 Dec 2016 17:39:41 -0500 Subject: [PATCH 177/595] working batch requests --- .../rbac-selected-list.directive.js | 33 ++++++++-- .../rbac-user-team.controller.js | 66 ++++++++++++++----- .../rbac-user-team.partial.html | 19 ++++-- .../rbac-multiselect-list.directive.js | 8 ++- .../rbac-role-column/roleList.directive.js | 2 +- .../list-generator/list-generator.factory.js | 8 ++- 6 files changed, 103 insertions(+), 33 deletions(-) diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js index 131a060523..99e2cb451f 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js @@ -17,7 +17,6 @@ export default ['$compile','templateUrl', 'i18n', 'generateList', selected: "=" }, link: function(scope, element, attrs) { - console.log(scope.resourceType) let listMap, list, list_html; listMap = { @@ -28,12 +27,12 @@ export default ['$compile','templateUrl', 'i18n', 'generateList', credentials: CredentialList }; - list = _.cloneDeep(listMap[scope.resourceType]) + list = _.cloneDeep(listMap[scope.resourceType]); list.fieldActions = { remove: { ngClick: `removeSelection(${list.iterator}, resourceType)`, - icon: 'fa-remove', + iconClass: 'fa fa-times-circle', awToolTip: i18n._(`Remove ${list.iterator}`), label: i18n._('Remove'), class: 'btn-sm' @@ -43,6 +42,8 @@ export default ['$compile','templateUrl', 'i18n', 'generateList', list.listTitleBadge = false; + // @issue - fix field.columnClass values for this view + switch(scope.resourceType){ case 'projects': @@ -77,8 +78,7 @@ export default ['$compile','templateUrl', 'i18n', 'generateList', description: list.fields.description }; break; - - default: + case 'credentials': list.fields = { name: list.fields.name, description: list.fields.description @@ -93,12 +93,31 @@ export default ['$compile','templateUrl', 'i18n', 'generateList', related: false, title: false, showSearch: false, + showEmptyPanel: false, paginate: false }); scope.list = list; - scope[`${list.iterator}_dataset`] = scope.collection; - scope[list.name] = scope.collection; + + scope.$watchCollection('collection', function(selected){ + scope[`${list.iterator}_dataset`] = scope.collection; + scope[list.name] = _.values(scope.collection); + }); + + scope.removeSelection = function(resource, type){ + let multiselect_scope, deselectedIdx; + + delete scope.collection[resource.id]; + delete scope.selected[type][resource.id]; + + // a quick & dirty hack + // section 1 and section 2 elements produce sibling scopes + // This means events propogated from section 2 are not received in section 1 + // The following code directly accesses the right scope by list table id + multiselect_scope = angular.element('#AddPermissions-body').find(`#${type}_table`).scope() + deselectedIdx = _.findIndex(multiselect_scope[type], {id: resource.id}); + multiselect_scope[type][deselectedIdx].isSelected = false; + }; element.append(list_html); $compile(element.contents())(scope); diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js index 3e41460487..665f5f6c26 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js @@ -11,8 +11,8 @@ * Controller for handling permissions adding */ -export default ['$rootScope', '$scope', '$state', 'GetBasePath', 'Rest', '$q', 'Wait', 'ProcessErrors', -function(rootScope, scope, $state, GetBasePath, Rest, $q, Wait, ProcessErrors) { +export default ['$rootScope', '$scope', '$state', 'i18n', 'CreateSelect2', 'GetBasePath', 'Rest', '$q', 'Wait', 'ProcessErrors', +function(rootScope, scope, $state, i18n, CreateSelect2, GetBasePath, Rest, $q, Wait, ProcessErrors) { init(); @@ -24,12 +24,20 @@ function(rootScope, scope, $state, GetBasePath, Rest, $q, Wait, ProcessErrors) { // selected - keyed by type of resource // selected[type] - keyed by each resource object's id // selected[type][id] === { roles: [ ... ], ... } + + // collection of resources selected in section 1 scope.selected = {}; - _.each(resources, (resource) => scope.selected[resource] = {}); + _.each(resources, (type) => scope.selected[type] = {}); + // collection of assignable roles per type of resource scope.keys = {}; - _.each(resources, (resource) => scope.keys[resource] = {}); + _.each(resources, (type) => scope.keys[type] = {}); + // collection of currently-selected role to assign in section 2 + scope.roleSelection = {}; + _.each(resources, (type) => scope.roleSelection[type] = null); + + // tracks currently-selected tabs, initialized with the job templates tab open scope.tab = { job_templates: true, workflow_templates: false, @@ -37,13 +45,39 @@ function(rootScope, scope, $state, GetBasePath, Rest, $q, Wait, ProcessErrors) { inventories: false, credentials: false }; + + // initializes select2 per select field + // html snippet: + /* +
    + +
    + */ + _.each(resources, (type) => buildSelect2(type)); + + function buildSelect2(type){ + CreateSelect2({ + element: `#${type}-role-select`, + multiple: false, + placeholder: i18n._('Select a role') + }); + } + scope.showKeyPane = false; + + // the user or team being assigned permissions scope.owner = scope.resolve.resourceData.data; } // aggregate name/descriptions for each available role, based on resource type + // reasoning: function aggregateKey(item, type){ - _.merge(scope.keys[type], item.summary_fields.object_roles); + _.merge(scope.keys[type], _.omit(item.summary_fields.object_roles, 'read_role')); } scope.closeModal = function() { @@ -66,11 +100,6 @@ function(rootScope, scope, $state, GetBasePath, Rest, $q, Wait, ProcessErrors) { return Object.keys(scope.selected[tab]).length > 0; }; - scope.removeSelection = function(resource, type){ - delete scope.selected[type][resource.id]; - resource.isSelected = false; - }; - // handle form tab changes scope.selectTab = function(selected){ _.each(scope.tab, (value, key, collection) => { @@ -78,7 +107,7 @@ function(rootScope, scope, $state, GetBasePath, Rest, $q, Wait, ProcessErrors) { }); }; - // pop/push into unified collection of selected users & teams + // pop/push into unified collection of selected resourcesf scope.$on("selectedOrDeselected", function(e, value) { let resourceType = scope.currentTab(), item = value.value; @@ -94,15 +123,20 @@ function(rootScope, scope, $state, GetBasePath, Rest, $q, Wait, ProcessErrors) { // post roles to api scope.saveForm = function() { - Wait('start'); - // scope.selected => { n: {id: n}, ... } => [ {id: n}, ... ] - let requests = _(scope.selected).map((type) => { - return _.map(type, (resource) => resource.roles); + //Wait('start'); + + // builds an array of role entities to apply to current user or team + let roles = _(scope.selected).map( (resources, type) =>{ + return _.map(resources, (resource) => { + return resource.summary_fields.object_roles[scope.roleSelection[type]] + }); }).flattenDeep().value(); + + debugger; Rest.setUrl(scope.owner.related.roles); - $q.all( _.map(requests, (entity) => Rest.post({id: entity.id})) ) + $q.all( _.map(roles, (entity) => Rest.post({id: entity.id})) ) .then( () =>{ Wait('stop'); scope.closeModal(); diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html index dc330bc035..de79fe03f0 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html @@ -20,7 +20,7 @@
    -
    +
    @@ -65,7 +65,7 @@
    -
    +
    @@ -141,7 +141,15 @@
    - + +
    + +
    @@ -149,7 +157,8 @@ + selected="selected" + ng-show="tab[type]">
    @@ -167,7 +176,7 @@
    diff --git a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js index d94411ff0f..855c15e093 100644 --- a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js +++ b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js @@ -70,7 +70,13 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL description: list.fields.description }; break; - + case 'Users': + list.fields = { + username: list.fields.username, + first_name: list.fields.first_name, + last_name: list.fields.last_name + } + break; default: list.fields = { name: list.fields.name, diff --git a/awx/ui/client/src/access/rbac-role-column/roleList.directive.js b/awx/ui/client/src/access/rbac-role-column/roleList.directive.js index 2ee33b8652..97682d0bc9 100644 --- a/awx/ui/client/src/access/rbac-role-column/roleList.directive.js +++ b/awx/ui/client/src/access/rbac-role-column/roleList.directive.js @@ -5,7 +5,7 @@ export default return { restrict: 'E', scope: true, - templateUrl: templateUrl('access/rbac-role-list/roleList'), + templateUrl: templateUrl('access/rbac-role-column/roleList'), link: function(scope, element, attrs) { // given a list of roles (things like "project // auditor") which are pulled from two different diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index b1a74db51a..cac1e129aa 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -241,9 +241,11 @@ export default ['$location', '$compile', '$rootScope', 'Attr', 'Icon', } // Show the "no items" box when loading is done and the user isn't actively searching and there are no results - html += `
    `; - html += (list.emptyListText) ? list.emptyListText : i18n._("PLEASE ADD ITEMS TO THIS LIST"); - html += "
    "; + if (options.showEmptyPanel === undefined || options.showEmptyPanel === true){ + html += `
    `; + html += (list.emptyListText) ? list.emptyListText : i18n._("PLEASE ADD ITEMS TO THIS LIST"); + html += "
    "; + } // Add a title and optionally a close button (used on Inventory->Groups) if (options.mode !== 'lookup' && list.showTitle) { From f6303d206bd2ebbed41654ca33bfe31a76c4e830 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Wed, 14 Dec 2016 17:48:56 -0500 Subject: [PATCH 178/595] add "Add Permissions" action to Teams form --- awx/ui/client/src/forms/Teams.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/forms/Teams.js b/awx/ui/client/src/forms/Teams.js index 639a4b2a00..39e5099b2f 100644 --- a/awx/ui/client/src/forms/Teams.js +++ b/awx/ui/client/src/forms/Teams.js @@ -150,7 +150,16 @@ export default ngShow: 'permission.summary_fields.user_capabilities.unattach' } }, - //hideOnSuperuser: true // defunct with RBAC + actions: { + add: { + ngClick: "$state.go('.add')", + label: 'Add', + awToolTip: i18n._('Grant Permission'), + actionClass: 'btn List-buttonSubmit', + buttonContent: '+ ' + i18n._('ADD PERMISSIONS'), + ngShow: '(puser_obj.summary_fields.user_capabilities.edit || canAdd)' + } + } } }, };}]); //InventoryForm From 26de1ffc2ff09cbe3443659febe2429afeec026f Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Wed, 14 Dec 2016 17:51:38 -0500 Subject: [PATCH 179/595] Filter batch permission granting states by role level --- awx/ui/client/src/shared/stateDefinitions.factory.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index 21ba01aade..34b3c1c67a 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -239,23 +239,23 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat url: '/add-permissions', params: { project_search: { - value: {order_by: 'name', page_size: '5'}, + value: {order_by: 'name', page_size: '5', role_level: 'admin_role'}, dynamic: true }, job_template_search: { - value: {order_by: 'name', page_size: '5'}, + value: {order_by: 'name', page_size: '5', role_level: 'admin_role'}, dynamic: true }, workflow_template_search: { - value: {order_by: 'name', page_size: '5'}, + value: {order_by: 'name', page_size: '5', role_level: 'admin_role'}, dynamic: true }, inventory_search: { - value: {order_by: 'name', page_size: '5'}, + value: {order_by: 'name', page_size: '5', role_level: 'admin_role'}, dynamic: true }, credential_search: { - value: {order_by: 'name', page_size: '5'}, + value: {order_by: 'name', page_size: '5', role_level: 'admin_role'}, dynamic: true } }, From ad89483c1efa510e82cdbc3de2273607c9effe13 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Thu, 15 Dec 2016 14:55:57 -0500 Subject: [PATCH 180/595] remove extra closing div --- awx/ui/client/src/shared/form-generator.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 0d66226088..e2aa526257 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1760,8 +1760,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat if (this.form.horizontal) { html += "
    \n"; } - html += '
    '; - } } From 55827611b6d8bec6b313ecc27b3a3d9ab4c31b44 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 15 Dec 2016 15:37:56 -0500 Subject: [PATCH 181/595] filter out ansible_* facts from provision callback extra_vars related to #4358 --- awx/api/views.py | 5 ++++- awx/main/utils/common.py | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 826941ec5f..f19b61e4e9 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -65,6 +65,9 @@ from awx.api.generics import * # noqa from awx.conf.license import get_license, feature_enabled, feature_exists, LicenseForbids from awx.main.models import * # noqa from awx.main.utils import * # noqa +from awx.main.utils import ( + callback_filter_out_ansible_extra_vars +) from awx.api.permissions import * # noqa from awx.api.renderers import * # noqa from awx.api.serializers import * # noqa @@ -2663,7 +2666,7 @@ class JobTemplateCallback(GenericAPIView): # Send a signal to celery that the job should be started. kv = {"inventory_sources_already_updated": inventory_sources_already_updated} if extra_vars is not None: - kv['extra_vars'] = extra_vars + kv['extra_vars'] = callback_filter_out_ansible_extra_vars(extra_vars) result = job.signal_start(**kv) if not result: data = dict(msg=_('Error starting job!')) diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 00937a84c1..bed0588287 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -43,7 +43,8 @@ __all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore', 'copy_m2m_relationships' ,'cache_list_capabilities', 'to_python_boolean', 'ignore_inventory_computed_fields', 'ignore_inventory_group_removal', '_inventory_updates', 'get_pk_from_dict', 'getattrd', 'NoDefaultProvided', - 'get_current_apps', 'set_current_apps', 'OutputEventFilter'] + 'get_current_apps', 'set_current_apps', 'OutputEventFilter', + 'callback_filter_out_ansible_extra_vars',] def get_object_or_400(klass, *args, **kwargs): @@ -824,3 +825,12 @@ class OutputEventFilter(object): self._current_event_data = next_event_data else: self._current_event_data = None + + +def callback_filter_out_ansible_extra_vars(extra_vars): + extra_vars_redacted = {} + for key, value in extra_vars.iteritems(): + if not key.startswith('ansible_'): + extra_vars_redacted[key] = value + return extra_vars_redacted + From 594f643fd8a19ee59dc9860d20d5a77ba0c5e2bf Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 15 Dec 2016 15:39:17 -0500 Subject: [PATCH 182/595] Fix projects modal in jt add/edit --- awx/ui/client/src/lists/Projects.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/lists/Projects.js b/awx/ui/client/src/lists/Projects.js index 83cb2eb7e7..1417201824 100644 --- a/awx/ui/client/src/lists/Projects.js +++ b/awx/ui/client/src/lists/Projects.js @@ -30,7 +30,8 @@ export default dataPlacement: 'right', icon: "icon-job-{{ project.statusIcon }}", columnClass: "List-staticColumn--smallStatus", - nosort: true + nosort: true, + excludeModal: true }, name: { key: true, From b6e2e3da70aa687ae6a554884e48a9840903957a Mon Sep 17 00:00:00 2001 From: Chris Church Date: Thu, 15 Dec 2016 16:03:00 -0500 Subject: [PATCH 183/595] Add new ansi_download format to download stdout and preserve ANSI escape sequences. --- awx/api/renderers.py | 5 +++ awx/api/templates/api/unified_job_stdout.md | 6 ++- awx/api/views.py | 45 ++++++++++++++++++--- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/awx/api/renderers.py b/awx/api/renderers.py index 9f3d17470e..fa039a2226 100644 --- a/awx/api/renderers.py +++ b/awx/api/renderers.py @@ -80,3 +80,8 @@ class AnsiTextRenderer(PlainTextRenderer): media_type = 'text/plain' format = 'ansi' + + +class AnsiDownloadRenderer(PlainTextRenderer): + + format = "ansi_download" diff --git a/awx/api/templates/api/unified_job_stdout.md b/awx/api/templates/api/unified_job_stdout.md index 63f7acea8e..d86c6e2378 100644 --- a/awx/api/templates/api/unified_job_stdout.md +++ b/awx/api/templates/api/unified_job_stdout.md @@ -13,6 +13,7 @@ Use the `format` query string parameter to specify the output format. * Plain Text with ANSI color codes: `?format=ansi` * JSON structure: `?format=json` * Downloaded Plain Text: `?format=txt_download` +* Downloaded Plain Text with ANSI color codes: `?format=ansi_download` (_New in Ansible Tower 2.0.0_) When using the Browsable API, HTML and JSON formats, the `start_line` and `end_line` query string parameters can be used @@ -21,7 +22,8 @@ to specify a range of line numbers to retrieve. Use `dark=1` or `dark=0` as a query string parameter to force or disable a dark background. -+Files over {{ settings.STDOUT_MAX_BYTES_DISPLAY|filesizeformat }} (configurable) will not display in the browser. Use the `txt_download` -+format to download the file directly to view it. +Files over {{ settings.STDOUT_MAX_BYTES_DISPLAY|filesizeformat }} (configurable) +will not display in the browser. Use the `txt_download` or `ansi_download` +formats to download the file directly to view it. {% include "api/_new_in_awx.md" %} diff --git a/awx/api/views.py b/awx/api/views.py index 826941ec5f..60e091b171 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -3897,20 +3897,47 @@ class UnifiedJobList(ListAPIView): new_in_148 = True +class StdoutANSIFilter(object): + + def __init__(self, fileobj): + self.fileobj = fileobj + self.extra_data = '' + if hasattr(fileobj,'close'): + self.close = fileobj.close + + def read(self, size=-1): + data = self.extra_data + while size > 0 and len(data) < size: + line = self.fileobj.readline(size) + if not line: + break + # Remove ANSI escape sequences used to embed event data. + line = re.sub(r'\x1b\[K(?:[A-Za-z0-9+/=]+\x1b\[\d+D)+\x1b\[K', '', line) + # Remove ANSI color escape sequences. + line = re.sub(r'\x1b[^m]*m', '', line) + data += line + if size > 0 and len(data) > size: + self.extra_data = data[size:] + data = data[:size] + else: + self.extra_data = '' + return data + + class UnifiedJobStdout(RetrieveAPIView): authentication_classes = [TokenGetAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES serializer_class = UnifiedJobStdoutSerializer renderer_classes = [BrowsableAPIRenderer, renderers.StaticHTMLRenderer, PlainTextRenderer, AnsiTextRenderer, - renderers.JSONRenderer, DownloadTextRenderer] + renderers.JSONRenderer, DownloadTextRenderer, AnsiDownloadRenderer] filter_backends = () new_in_148 = True def retrieve(self, request, *args, **kwargs): unified_job = self.get_object() obj_size = unified_job.result_stdout_size - if request.accepted_renderer.format != 'txt_download' and obj_size > settings.STDOUT_MAX_BYTES_DISPLAY: + if request.accepted_renderer.format not in {'txt_download', 'ansi_download'} and obj_size > settings.STDOUT_MAX_BYTES_DISPLAY: response_message = _("Standard Output too large to display (%(text_size)d bytes), " "only download supported for sizes over %(supported_size)d bytes") % { 'text_size': obj_size, 'supported_size': settings.STDOUT_MAX_BYTES_DISPLAY} @@ -3951,18 +3978,24 @@ class UnifiedJobStdout(RetrieveAPIView): elif content_format == 'html': return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': body}) return Response(data) + elif request.accepted_renderer.format == 'txt': + return Response(unified_job.result_stdout) elif request.accepted_renderer.format == 'ansi': return Response(unified_job.result_stdout_raw) - elif request.accepted_renderer.format == 'txt_download': + elif request.accepted_renderer.format in {'txt_download', 'ansi_download'}: try: content_fd = open(unified_job.result_stdout_file, 'r') + if request.accepted_renderer.format == 'txt_download': + # For txt downloads, filter out ANSI escape sequences. + content_fd = StdoutANSIFilter(content_fd) + suffix = '' + else: + suffix = '_ansi' response = HttpResponse(FileWrapper(content_fd), content_type='text/plain') - response["Content-Disposition"] = 'attachment; filename="job_%s.txt"' % str(unified_job.id) + response["Content-Disposition"] = 'attachment; filename="job_%s%s.txt"' % (str(unified_job.id), suffix) return response except Exception as e: return Response({"error": _("Error generating stdout download file: %s") % str(e)}, status=status.HTTP_400_BAD_REQUEST) - elif request.accepted_renderer.format == 'txt': - return Response(unified_job.result_stdout) else: return super(UnifiedJobStdout, self).retrieve(request, *args, **kwargs) From 11a1ca987f5f48be516d4ced830ed7f799f33332 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Thu, 15 Dec 2016 16:06:52 -0500 Subject: [PATCH 184/595] remove debugger --- .../src/access/add-rbac-user-team/rbac-user-team.controller.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js index 665f5f6c26..3e8f637f1d 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js @@ -131,8 +131,6 @@ function(rootScope, scope, $state, i18n, CreateSelect2, GetBasePath, Rest, $q, W return resource.summary_fields.object_roles[scope.roleSelection[type]] }); }).flattenDeep().value(); - - debugger; Rest.setUrl(scope.owner.related.roles); From da5ff5908120fddde7ee1eb42430cf37fad82e71 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 15 Dec 2016 16:08:03 -0500 Subject: [PATCH 185/595] Removed extra appendage of id to the edit user url --- awx/ui/client/src/controllers/Users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/controllers/Users.js b/awx/ui/client/src/controllers/Users.js index 9aade4fbf8..9a0aa2ea76 100644 --- a/awx/ui/client/src/controllers/Users.js +++ b/awx/ui/client/src/controllers/Users.js @@ -311,7 +311,7 @@ export function UsersEdit($scope, $rootScope, $location, $scope.formSave = function() { $rootScope.flashMessage = null; if ($scope[form.name + '_form'].$valid) { - Rest.setUrl(defaultUrl + id + '/'); + Rest.setUrl(defaultUrl + '/'); var data = processNewData(form.fields); Rest.put(data).success(function() { $state.go($state.current, null, { reload: true }); From ef1f77bf8e9fe4f72b9ff7e84cf35fce59417aa1 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 15 Dec 2016 16:12:59 -0500 Subject: [PATCH 186/595] fix what I broke with the job through cancel proj update --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 2ad3343607..63128635ab 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1057,7 +1057,7 @@ class RunJob(BaseTask): project_update_task = local_project_sync._get_task_class() try: project_update_task().run(local_project_sync.id) - job = self.update_model(job.pk, scm_revision=project.scm_revision) + job = self.update_model(job.pk, scm_revision=job.project.scm_revision) except Exception: job = self.update_model(job.pk, status='failed', job_explanation=('Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % From 888ec25c3cc529fb484ae108261991e3869c192a Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 15 Dec 2016 16:22:06 -0500 Subject: [PATCH 187/595] Adding new privilege escalation methods from core --- awx/main/migrations/0034_v310_release.py | 5 +++++ awx/main/models/credential.py | 2 ++ awx/main/tasks.py | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/awx/main/migrations/0034_v310_release.py b/awx/main/migrations/0034_v310_release.py index fa46beec20..c3849468f8 100644 --- a/awx/main/migrations/0034_v310_release.py +++ b/awx/main/migrations/0034_v310_release.py @@ -47,6 +47,11 @@ class Migration(migrations.Migration): name='uuid', field=models.CharField(max_length=40), ), + migrations.AlterField( + model_name='credential', + name='become_method', + field=models.CharField(default=b'', help_text='Privilege escalation method.', max_length=32, blank=True, choices=[(b'', 'None'), (b'sudo', 'Sudo'), (b'su', 'Su'), (b'pbrun', 'Pbrun'), (b'pfexec', 'Pfexec'), (b'dzdo', 'DZDO'), (b'pmrun', 'Pmrun')]), + ), # Add Workflows migrations.AlterField( model_name='unifiedjob', diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index aa0bf3243c..a7f77e87c2 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -50,6 +50,8 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): ('su', _('Su')), ('pbrun', _('Pbrun')), ('pfexec', _('Pfexec')), + ('dzdo', _('DZDO')), + ('pmrun', _('Pmrun')), #('runas', _('Runas')), ] diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 63128635ab..de69a3ecd5 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1743,6 +1743,10 @@ class RunAdHocCommand(BaseTask): d[re.compile(r'^pfexec password.*:\s*?$', re.M)] = 'become_password' d[re.compile(r'^RUNAS password.*:\s*?$', re.M)] = 'become_password' d[re.compile(r'^runas password.*:\s*?$', re.M)] = 'become_password' + d[re.compile(r'^DZDO password.*:\s*?$', re.M)] = 'become_password' + d[re.compile(r'^dzdo password.*:\s*?$', re.M)] = 'become_password' + d[re.compile(r'^PMRUN password.*:\s*?$', re.M)] = 'become_password' + d[re.compile(r'^pmrun password.*:\s*?$', re.M)] = 'become_password' d[re.compile(r'^SSH password:\s*?$', re.M)] = 'ssh_password' d[re.compile(r'^Password:\s*?$', re.M)] = 'ssh_password' return d From 4509f402a8a645c0ef6d219678d07b019bfb104c Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 15 Dec 2016 16:31:39 -0500 Subject: [PATCH 188/595] send notification when recifiying celery jobs --- awx/main/scheduler/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/scheduler/__init__.py b/awx/main/scheduler/__init__.py index efba3fdb75..e92eb429e1 100644 --- a/awx/main/scheduler/__init__.py +++ b/awx/main/scheduler/__init__.py @@ -346,6 +346,7 @@ class TaskManager(): 'Celery, so it has been marked as failed.', )) task_obj.save() + _send_notification_templates(task_obj, 'failed') connection.on_commit(lambda: task_obj.websocket_emit_status('failed')) logger.error("Task %s appears orphaned... marking as failed" % task) From 851dacd407542210a9557a15b703d4462ff86a9d Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 15 Dec 2016 16:44:10 -0500 Subject: [PATCH 189/595] Fixed recently used job templates edit link --- .../lists/job-templates/job-templates-list.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.directive.js b/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.directive.js index 928f03a1c4..82aca5b377 100644 --- a/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.directive.js +++ b/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.directive.js @@ -47,7 +47,7 @@ export default }; scope.editJobTemplate = function (jobTemplateId) { - $state.go('templates.editJobTemplate', {id: jobTemplateId}); + $state.go('templates.editJobTemplate', {job_template_id: jobTemplateId}); }; } }]; From b78ed36abf3e9e05cb56517bdb57408a5ca49be5 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Thu, 15 Dec 2016 13:47:12 -0800 Subject: [PATCH 190/595] post processing for scheduled jobs list --- awx/ui/client/src/controllers/Schedules.js | 2 +- awx/ui/client/src/lists/ScheduledJobs.js | 3 +- awx/ui/client/src/scheduler/main.js | 54 +++++++++++++- .../src/scheduler/schedulerList.controller.js | 72 ++++++++++++++----- 4 files changed, 106 insertions(+), 25 deletions(-) diff --git a/awx/ui/client/src/controllers/Schedules.js b/awx/ui/client/src/controllers/Schedules.js index edebf66a84..5a29150215 100644 --- a/awx/ui/client/src/controllers/Schedules.js +++ b/awx/ui/client/src/controllers/Schedules.js @@ -65,7 +65,7 @@ GetBasePath, Wait, Find, LoadSchedulesScope, GetChoices) { msg: 'Call to ' + url + ' failed. GET returned: ' + status }); }); }); - + debugger; $scope.refreshJobs = function() { // @issue: OLD SEARCH // $scope.search(SchedulesList.iterator); diff --git a/awx/ui/client/src/lists/ScheduledJobs.js b/awx/ui/client/src/lists/ScheduledJobs.js index e4a17496c6..a771a75b8f 100644 --- a/awx/ui/client/src/lists/ScheduledJobs.js +++ b/awx/ui/client/src/lists/ScheduledJobs.js @@ -31,8 +31,7 @@ export default name: { label: i18n._('Name'), columnClass: 'col-lg-4 col-md-5 col-sm-5 col-xs-7 List-staticColumnAdjacent', - sourceModel: 'unified_job_template', - sourceField: 'name', + ngBind: 'schedule.summary_fields.unified_job_template.name', ngClick: "editSchedule(schedule)", awToolTip: "{{ schedule.nameTip | sanitize}}", dataTipWatch: 'schedule.nameTip', diff --git a/awx/ui/client/src/scheduler/main.js b/awx/ui/client/src/scheduler/main.js index 840197d944..f8620c880e 100644 --- a/awx/ui/client/src/scheduler/main.js +++ b/awx/ui/client/src/scheduler/main.js @@ -45,7 +45,19 @@ export default let path = `${GetBasePath('job_templates')}${$stateParams.id}`; Rest.setUrl(path); return Rest.get(path).then((res) => res.data); - }] + }], + UnifiedJobsOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', + function(Rest, GetBasePath, $stateParams, $q) { + Rest.setUrl(GetBasePath('unified_jobs')); + var val = $q.defer(); + Rest.options() + .then(function(data) { + val.resolve(data.data); + }, function(data) { + val.reject(data); + }); + return val.promise; + }] }, views: { '@': { @@ -119,7 +131,19 @@ export default let path = `${GetBasePath('workflow_job_templates')}${$stateParams.id}`; Rest.setUrl(path); return Rest.get(path).then((res) => res.data); - }] + }], + UnifiedJobsOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', + function(Rest, GetBasePath, $stateParams, $q) { + Rest.setUrl(GetBasePath('unified_jobs')); + var val = $q.defer(); + Rest.options() + .then(function(data) { + val.resolve(data.data); + }, function(data) { + val.reject(data); + }); + return val.promise; + }] }, views: { '@': { @@ -190,7 +214,19 @@ export default let path = `${GetBasePath('projects')}${$stateParams.id}`; Rest.setUrl(path); return Rest.get(path).then((res) => res.data); - }] + }], + UnifiedJobsOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', + function(Rest, GetBasePath, $stateParams, $q) { + Rest.setUrl(GetBasePath('unified_jobs')); + var val = $q.defer(); + Rest.options() + .then(function(data) { + val.resolve(data.data); + }, function(data) { + val.reject(data); + }); + return val.promise; + }] }, views: { '@': { @@ -268,6 +304,18 @@ export default } ], ParentObject: [() =>{return {endpoint:'/api/v1/schedules'}; }], + UnifiedJobsOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', + function(Rest, GetBasePath, $stateParams, $q) { + Rest.setUrl(GetBasePath('unified_jobs')); + var val = $q.defer(); + Rest.options() + .then(function(data) { + val.resolve(data.data); + }, function(data) { + val.reject(data); + }); + return val.promise; + }] }, views: { 'list@jobs': { diff --git a/awx/ui/client/src/scheduler/schedulerList.controller.js b/awx/ui/client/src/scheduler/schedulerList.controller.js index e003ea1fd2..4f9e9809d3 100644 --- a/awx/ui/client/src/scheduler/schedulerList.controller.js +++ b/awx/ui/client/src/scheduler/schedulerList.controller.js @@ -14,12 +14,12 @@ export default [ '$scope', '$compile', '$location', '$stateParams', 'SchedulesList', 'Rest', 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'GetBasePath', 'Wait', 'rbacUiControlService', - 'Find', 'ToggleSchedule', 'DeleteSchedule', 'GetChoices', '$q', '$state', 'Dataset', 'ParentObject', + 'Find', 'ToggleSchedule', 'DeleteSchedule', 'GetChoices', '$q', '$state', 'Dataset', 'ParentObject', 'UnifiedJobsOptions', function($scope, $compile, $location, $stateParams, SchedulesList, Rest, ProcessErrors, ReturnToCaller, ClearScope, GetBasePath, Wait, rbacUiControlService, Find, ToggleSchedule, DeleteSchedule, GetChoices, - $q, $state, Dataset, ParentObject) { + $q, $state, Dataset, ParentObject, UnifiedJobsOptions) { ClearScope(); @@ -43,11 +43,45 @@ export default [ $scope.list = list; $scope[`${list.iterator}_dataset`] = Dataset.data; $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + $scope.unified_job_options = UnifiedJobsOptions.actions.GET; - _.forEach($scope[list.name], buildTooltips); + // _.forEach($scope[list.name], buildTooltips); + } + + $scope.$on(`${list.iterator}_options`, function(event, data){ + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); + }); + + $scope.$watchCollection(`${$scope.list.name}`, function() { + optionsRequestDataProcessing(); + } + ); + + // iterate over the list and add fields like type label, after the + // OPTIONS request returns, or the list is sorted/paginated/searched + function optionsRequestDataProcessing(){ + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; + + // Set the item type label + if (list.fields.type && $scope.unified_job_options && + $scope.unified_job_options.hasOwnProperty('type')) { + $scope.unified_job_options.type.choices.every(function(choice) { + if (choice[0] === itm.summary_fields.unified_job_template.unified_job_type) { + itm.type_label = choice[1]; + return false; + } + return true; + }); + } + buildTooltips(itm); + + }); } function buildTooltips(schedule) { + var job = schedule.summary_fields.unified_job_template; if (schedule.enabled) { schedule.play_tip = 'Schedule is active. Click to stop.'; schedule.status = 'active'; @@ -57,6 +91,18 @@ export default [ schedule.status = 'stopped'; schedule.status_tip = 'Schedule is stopped. Click to activate.'; } + + schedule.nameTip = schedule.name; + // include the word schedule if the schedule name does not include the word schedule + if (schedule.name.indexOf("schedule") === -1 && schedule.name.indexOf("Schedule") === -1) { + schedule.nameTip += " schedule"; + } + schedule.nameTip += " for "; + if (job.name.indexOf("job") === -1 && job.name.indexOf("Job") === -1) { + schedule.nameTip += "job "; + } + schedule.nameTip += job.name; + schedule.nameTip += ". Click to edit schedule."; } $scope.refreshSchedules = function() { @@ -99,7 +145,7 @@ export default [ name: 'projectSchedules.edit', params: { id: schedule.unified_job_template, - schedule_id: schedule.id + schedule_id: schedule.id } }); break; @@ -109,7 +155,7 @@ export default [ name: 'managementJobSchedules.edit', params: { id: schedule.unified_job_template, - schedule_id: schedule.id + schedule_id: schedule.id } }); break; @@ -136,7 +182,7 @@ export default [ throw err; } }); - }); + }); } }; @@ -160,7 +206,7 @@ export default [ }; base = $location.path().replace(/^\//, '').split('/')[0]; - + if (base === 'management_jobs') { $scope.base = base = 'system_job_templates'; } @@ -175,17 +221,5 @@ export default [ $scope.formCancel = function() { $state.go('^', null, { reload: true }); }; - - // @issue - believe this is no longer necessary now that parent object is resolved prior to controller initilizing - - // Wait('start'); - - // GetChoices({ - // scope: $scope, - // url: GetBasePath('unified_jobs'), //'/static/sample/data/types/data.json' - // field: 'type', - // variable: 'type_choices', - // callback: 'choicesReady' - // }); } ]; From 0504348d6971ae5f172be0f9422c91e4bc70d01a Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 15 Dec 2016 16:59:06 -0500 Subject: [PATCH 191/595] Removed Job Templates and Workflow Job Templates from the activity stream dropdown in favor of the combined Templates --- .../streamDropdownNav/stream-dropdown-nav.directive.js | 4 +--- awx/ui/client/src/helpers/ActivityStream.js | 6 ------ 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js index cba0ecaddf..dc6c4a819d 100644 --- a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js +++ b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js @@ -20,15 +20,13 @@ export default ['templateUrl', function(templateUrl) { {label: 'Hosts', value: 'host'}, {label: 'Inventories', value: 'inventory'}, {label: 'Inventory Scripts', value: 'inventory_script'}, - {label: 'Job Templates', value: 'job_template'}, {label: 'Jobs', value: 'job'}, {label: 'Organizations', value: 'organization'}, {label: 'Projects', value: 'project'}, {label: 'Schedules', value: 'schedule'}, {label: 'Teams', value: 'team'}, {label: 'Templates', value: 'template'}, - {label: 'Users', value: 'user'}, - {label: 'Workflow Job Templates', value: 'workflow_job_template'} + {label: 'Users', value: 'user'} ]; CreateSelect2({ diff --git a/awx/ui/client/src/helpers/ActivityStream.js b/awx/ui/client/src/helpers/ActivityStream.js index 7dc8c9ecc0..37da4f2857 100644 --- a/awx/ui/client/src/helpers/ActivityStream.js +++ b/awx/ui/client/src/helpers/ActivityStream.js @@ -25,9 +25,6 @@ export default case 'inventory': rtnTitle = 'INVENTORIES'; break; - case 'job_template': - rtnTitle = 'JOB TEMPLATES'; - break; case 'credential': rtnTitle = 'CREDENTIALS'; break; @@ -55,9 +52,6 @@ export default case 'template': rtnTitle = 'TEMPLATES'; break; - case 'workflow_job_template': - rtnTitle = 'WORKFLOW JOB TEMPLATES'; - break; } return rtnTitle; From 0662720da34e55c374a3e84a72e65fb7e362c3a2 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 15 Dec 2016 17:44:20 -0500 Subject: [PATCH 192/595] Set the url before attempting to update the team --- awx/ui/client/src/controllers/Teams.js | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/ui/client/src/controllers/Teams.js b/awx/ui/client/src/controllers/Teams.js index 0e3a47d855..464a10ee10 100644 --- a/awx/ui/client/src/controllers/Teams.js +++ b/awx/ui/client/src/controllers/Teams.js @@ -217,6 +217,7 @@ export function TeamsEdit($scope, $rootScope, $stateParams, $rootScope.flashMessage = null; if ($scope[form.name + '_form'].$valid) { var data = processNewData(form.fields); + Rest.setUrl(defaultUrl); Rest.put(data).success(function() { $state.go($state.current, null, { reload: true }); }) From 2b5d64d7468a518c64120758c575aef7654b7e87 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Thu, 15 Dec 2016 15:16:25 -0800 Subject: [PATCH 193/595] removing debugger statements --- awx/ui/client/src/controllers/Schedules.js | 2 +- awx/ui/client/src/templates/main.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/controllers/Schedules.js b/awx/ui/client/src/controllers/Schedules.js index 5a29150215..edebf66a84 100644 --- a/awx/ui/client/src/controllers/Schedules.js +++ b/awx/ui/client/src/controllers/Schedules.js @@ -65,7 +65,7 @@ GetBasePath, Wait, Find, LoadSchedulesScope, GetChoices) { msg: 'Call to ' + url + ' failed. GET returned: ' + status }); }); }); - debugger; + $scope.refreshJobs = function() { // @issue: OLD SEARCH // $scope.search(SchedulesList.iterator); diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index b458121def..03b380bd58 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -115,7 +115,7 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesA }, 'jobTemplateList@templates.editWorkflowJobTemplate.workflowMaker': { templateProvider: function(WorkflowMakerJobTemplateList, generateList) { - //debugger; + let html = generateList.build({ list: WorkflowMakerJobTemplateList, input_type: 'radio', From 25ee37fd3a801f061e36a7c7abe7e482205dde7f Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 16 Dec 2016 09:30:23 -0500 Subject: [PATCH 194/595] change session limit message --- awx/main/models/organization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 8f96d1656c..c2fe3b1c4f 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -210,7 +210,7 @@ class AuthToken(BaseModel): REASON_CHOICES = [ ('', _('Token not invalidated')), ('timeout_reached', _('Token is expired')), - ('limit_reached', _('Maximum per-user sessions reached')), + ('limit_reached', _('The maximum number of allowed sessions for this user has been exceeded.')), # invalid_token is not a used data-base value, but is returned by the # api when a token is not found ('invalid_token', _('Invalid token')), From a566cec5db50e3ddb6f64bce53ed0f127b84557c Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 16 Dec 2016 10:06:34 -0500 Subject: [PATCH 195/595] rectifiy arch name in fact scans --- awx/plugins/library/scan_packages.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/plugins/library/scan_packages.py b/awx/plugins/library/scan_packages.py index 13b28542f6..d5aafc66e6 100755 --- a/awx/plugins/library/scan_packages.py +++ b/awx/plugins/library/scan_packages.py @@ -22,19 +22,19 @@ EXAMPLES = ''' # { # "source": "apt", # "version": "1.0.6-5", -# "architecture": "amd64", +# "arch": "amd64", # "name": "libbz2-1.0" # }, # { # "source": "apt", # "version": "2.7.1-4ubuntu1", -# "architecture": "amd64", +# "arch": "amd64", # "name": "patch" # }, # { # "source": "apt", # "version": "4.8.2-19ubuntu1", -# "architecture": "amd64", +# "arch": "amd64", # "name": "gcc-4.8-base" # }, ... ] } } ''' @@ -64,7 +64,7 @@ def deb_package_list(): ac_pkg = apt_cache[package].installed package_details = dict(name=package, version=ac_pkg.version, - architecture=ac_pkg.architecture, + arch=ac_pkg.architecture, source='apt') installed_packages.append(package_details) return installed_packages From 5d9d332792439b851a28566adc6a4c974d96db81 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 16 Dec 2016 10:36:38 -0500 Subject: [PATCH 196/595] rackspace deprecation warning on inv update --- awx/main/management/commands/inventory_import.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 7f87694cbe..797a211b79 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -1253,6 +1253,11 @@ class Command(NoArgsCommand): except re.error: raise CommandError('invalid regular expression for --host-filter') + ''' + TODO: Remove this deprecation when we remove support for rax.py + ''' + self.logger.info("Rackspace inventory sync is Deprecated in Tower 3.1.0 and support for Rackspace will be removed in a future release.") + begin = time.time() self.load_inventory_from_database() From c8f1f11816594309b57a2275a823c441a8c215ad Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Fri, 16 Dec 2016 11:23:24 -0500 Subject: [PATCH 197/595] Fixed jshint errors --- .../rbac-selected-list.directive.js | 6 +++--- .../add-rbac-user-team/rbac-user-team.controller.js | 12 ++++++------ .../rbac-multiselect-list.directive.js | 12 ++++++------ awx/ui/client/src/forms/Teams.js | 3 +-- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js index 99e2cb451f..f65d0324ad 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-selected-list.directive.js @@ -5,7 +5,7 @@ *************************************************/ /* jshint unused: vars */ -export default ['$compile','templateUrl', 'i18n', 'generateList', +export default ['$compile','templateUrl', 'i18n', 'generateList', 'ProjectList', 'TemplateList', 'InventoryList', 'CredentialList', function($compile, templateUrl, i18n, generateList, ProjectList, TemplateList, InventoryList, CredentialList) { @@ -71,7 +71,7 @@ export default ['$compile','templateUrl', 'i18n', 'generateList', case 'workflow_templates': list.name = 'workflow_templates'; - list.iterator = 'workflow_template', + list.iterator = 'workflow_template'; list.basePath = 'workflow_job_templates'; list.fields = { name: list.fields.name, @@ -114,7 +114,7 @@ export default ['$compile','templateUrl', 'i18n', 'generateList', // section 1 and section 2 elements produce sibling scopes // This means events propogated from section 2 are not received in section 1 // The following code directly accesses the right scope by list table id - multiselect_scope = angular.element('#AddPermissions-body').find(`#${type}_table`).scope() + multiselect_scope = angular.element('#AddPermissions-body').find(`#${type}_table`).scope(); deselectedIdx = _.findIndex(multiselect_scope[type], {id: resource.id}); multiselect_scope[type][deselectedIdx].isSelected = false; }; diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js index 3e8f637f1d..3530124788 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js @@ -11,7 +11,7 @@ * Controller for handling permissions adding */ -export default ['$rootScope', '$scope', '$state', 'i18n', 'CreateSelect2', 'GetBasePath', 'Rest', '$q', 'Wait', 'ProcessErrors', +export default ['$rootScope', '$scope', '$state', 'i18n', 'CreateSelect2', 'GetBasePath', 'Rest', '$q', 'Wait', 'ProcessErrors', function(rootScope, scope, $state, i18n, CreateSelect2, GetBasePath, Rest, $q, Wait, ProcessErrors) { init(); @@ -51,7 +51,7 @@ function(rootScope, scope, $state, i18n, CreateSelect2, GetBasePath, Rest, $q, W /*