From dbca95fb2abc6c729f35e0b6e60e9e0e488e76b9 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 29 Sep 2016 10:37:18 -0400 Subject: [PATCH 001/260] Update postgres yum/apt repo locations *Thanks postgres team --- tools/docker-compose/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/docker-compose/Dockerfile b/tools/docker-compose/Dockerfile index 4e22788585..3acd687372 100644 --- a/tools/docker-compose/Dockerfile +++ b/tools/docker-compose/Dockerfile @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y software-properties-common python-softw RUN add-apt-repository -y ppa:chris-lea/redis-server; add-apt-repository -y ppa:chris-lea/zeromq; add-apt-repository -y ppa:chris-lea/node.js; add-apt-repository -y ppa:ansible/ansible; add-apt-repository -y ppa:jal233/proot; RUN curl -sL https://deb.nodesource.com/setup_0.12 | bash - RUN curl -sL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - -RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main" | tee /etc/apt/sources.list.d/postgres-9.4.list +RUN echo "deb http://download.postgresql.org/pub/repos/apt/dists/ trusty-pgdg main" | tee /etc/apt/sources.list.d/postgres-9.4.list RUN apt-get update && apt-get install -y openssh-server ansible mg vim tmux git mercurial subversion python-dev python-psycopg2 make postgresql-client libpq-dev nodejs python-psutil libxml2-dev libxslt-dev lib32z1-dev libsasl2-dev libldap2-dev libffi-dev libzmq-dev proot python-pip libxmlsec1-dev swig redis-server libgss-dev libkrb5-dev && apt-get autoremove --purge -y && rm -rf /var/lib/apt/lists/* RUN pip install flake8 pytest pytest-pythonpath pytest-django pytest-cov pytest-mock dateutils django-debug-toolbar==1.4 pyflakes==1.0.0 virtualenv RUN /usr/bin/ssh-keygen -q -t rsa -N "" -f /root/.ssh/id_rsa From 870f8d6b1ee8070f7e07d62a069acc95de7afac4 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 29 Sep 2016 13:18:24 -0400 Subject: [PATCH 002/260] Fix up deb packaging paths --- tools/docker-compose/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/docker-compose/Dockerfile b/tools/docker-compose/Dockerfile index 3acd687372..8ec83450b5 100644 --- a/tools/docker-compose/Dockerfile +++ b/tools/docker-compose/Dockerfile @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y software-properties-common python-softw RUN add-apt-repository -y ppa:chris-lea/redis-server; add-apt-repository -y ppa:chris-lea/zeromq; add-apt-repository -y ppa:chris-lea/node.js; add-apt-repository -y ppa:ansible/ansible; add-apt-repository -y ppa:jal233/proot; RUN curl -sL https://deb.nodesource.com/setup_0.12 | bash - RUN curl -sL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - -RUN echo "deb http://download.postgresql.org/pub/repos/apt/dists/ trusty-pgdg main" | tee /etc/apt/sources.list.d/postgres-9.4.list +RUN echo "deb http://download.postgresql.org/pub/repos/apt/ trusty-pgdg main" | tee /etc/apt/sources.list.d/postgres-9.4.list RUN apt-get update && apt-get install -y openssh-server ansible mg vim tmux git mercurial subversion python-dev python-psycopg2 make postgresql-client libpq-dev nodejs python-psutil libxml2-dev libxslt-dev lib32z1-dev libsasl2-dev libldap2-dev libffi-dev libzmq-dev proot python-pip libxmlsec1-dev swig redis-server libgss-dev libkrb5-dev && apt-get autoremove --purge -y && rm -rf /var/lib/apt/lists/* RUN pip install flake8 pytest pytest-pythonpath pytest-django pytest-cov pytest-mock dateutils django-debug-toolbar==1.4 pyflakes==1.0.0 virtualenv RUN /usr/bin/ssh-keygen -q -t rsa -N "" -f /root/.ssh/id_rsa From 5af2f51bd91f30fb4d865d134bb1aa801f830830 Mon Sep 17 00:00:00 2001 From: sundeep-co-in Date: Wed, 1 Feb 2017 22:04:08 +0530 Subject: [PATCH 003/260] add lang option in push/pull --- tools/scripts/manage_translations.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tools/scripts/manage_translations.py b/tools/scripts/manage_translations.py index 3611b0d4de..4f6ce0a4c5 100755 --- a/tools/scripts/manage_translations.py +++ b/tools/scripts/manage_translations.py @@ -24,11 +24,11 @@ # to update django.pot file, run: # $ python tools/scripts/manage_translations.py update # -# to update both pot files, run: +# to update both pot files locally, run: # $ python tools/scripts/manage_translations.py update --both # -# to push both pot files (update also), run: -# $ python tools/scripts/manage_translations.py push --both +# to push both pot files (update also) and ja translations, run: +# $ python tools/scripts/manage_translations.py push --both --lang ja # # to pull both translations for Japanese and French, run: # $ python tools/scripts/manage_translations.py pull --both --lang ja,fr @@ -128,17 +128,21 @@ def push(lang=None, both=None): (1) for angularjs - project_type should be gettext - {locale}.po format (2) for django - project_type should be podir - {locale}/{filename}.po format (3) only required languages should be kept enabled + This will update/overwrite PO file with translations found for input lang(s) + [!] POT and PO must remain in sync as messages would overwrite as per POT file """ - - command = "zanata push --project-config %(config)s --push-type both --force --disable-ssl-cert" + command = "zanata push --project-config %(config)s --push-type both --force --lang %(lang)s --disable-ssl-cert" + lang = lang[0] if lang and len(lang) > 0 else 'en-us' if both: - p = Popen(command % {'config': ZNTA_CONFIG_FRONTEND_TRANS}, stdout=PIPE, stderr=PIPE, shell=True) + p = Popen(command % {'config': ZNTA_CONFIG_FRONTEND_TRANS, 'lang': lang}, + stdout=PIPE, stderr=PIPE, shell=True) output, errors = p.communicate() if _handle_response(output, errors): _print_zanata_project_url(ZNTA_CONFIG_FRONTEND_TRANS) - p = Popen(command % {'config': ZNTA_CONFIG_BACKEND_TRANS}, stdout=PIPE, stderr=PIPE, shell=True) + p = Popen(command % {'config': ZNTA_CONFIG_BACKEND_TRANS, 'lang': lang}, + stdout=PIPE, stderr=PIPE, shell=True) output, errors = p.communicate() if _handle_response(output, errors): _print_zanata_project_url(ZNTA_CONFIG_BACKEND_TRANS) From c9a5163ab0284fe6bf9f55ae6e54c7c78c8cbcb3 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 1 Feb 2017 13:07:03 -0500 Subject: [PATCH 004/260] Fixed permissions multi-select issues --- .../rbac-resource.controller.js | 4 +- .../rbac-user-team.controller.js | 2 +- .../rbac-multiselect/permissionsUsers.list.js | 6 -- awx/ui/client/src/lists/Users.js | 6 -- .../linkout/addUsers/addUsers.controller.js | 61 +++++++++++++++--- .../linkout/addUsers/addUsers.directive.js | 6 +- .../linkout/addUsers/addUsers.partial.html | 3 +- .../organizations-admins.controller.js | 9 +-- .../organizations-users.controller.js | 12 ++-- .../linkout/organizations-linkout.route.js | 62 ------------------- .../select-list-item.directive.js | 2 +- 11 files changed, 69 insertions(+), 104 deletions(-) diff --git a/awx/ui/client/src/access/add-rbac-resource/rbac-resource.controller.js b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.controller.js index e40589a3f6..6e41696faa 100644 --- a/awx/ui/client/src/access/add-rbac-resource/rbac-resource.controller.js +++ b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.controller.js @@ -62,13 +62,13 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'Pr user.username; } - if (item.isSelected) { + if (value.isSelected) { if (item.type === 'user') { item.name = buildName(item); } scope.allSelected.push(item); } else { - scope.allSelected = _.remove(scope.allSelected, { id: item.id }); + _.remove(scope.allSelected, { id: item.id }); } }); 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 edbd5eaf6f..a97bd0218c 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 @@ -127,7 +127,7 @@ function(rootScope, scope, $state, i18n, CreateSelect2, GetBasePath, Rest, $q, W let resourceType = scope.currentTab(), item = value.value; - if (item.isSelected) { + if (value.isSelected) { scope.selected[resourceType][item.id] = item; scope.selected[resourceType][item.id].roles = []; aggregateKey(item, resourceType); diff --git a/awx/ui/client/src/access/rbac-multiselect/permissionsUsers.list.js b/awx/ui/client/src/access/rbac-multiselect/permissionsUsers.list.js index 9769df3506..39b083f06c 100644 --- a/awx/ui/client/src/access/rbac-multiselect/permissionsUsers.list.js +++ b/awx/ui/client/src/access/rbac-multiselect/permissionsUsers.list.js @@ -9,12 +9,6 @@ return { name: 'users', iterator: 'user', - defaultSearchParams: function(term){ - return {or__username__icontains: term, - or__first_name__icontains: term, - or__last_name__icontains: term - }; - }, title: false, listTitleBadge: false, multiSelect: true, diff --git a/awx/ui/client/src/lists/Users.js b/awx/ui/client/src/lists/Users.js index bfff119616..fb84286bfb 100644 --- a/awx/ui/client/src/lists/Users.js +++ b/awx/ui/client/src/lists/Users.js @@ -15,12 +15,6 @@ export default search: { order_by: 'username' }, - defaultSearchParams: function(term){ - return {or__username__icontains: term, - or__first_name__icontains: term, - or__last_name__icontains: term - }; - }, iterator: 'user', selectTitle: i18n._('Add Users'), editTitle: i18n._('Users'), 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 e576ac5fd2..0a59ddbaef 100644 --- a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js +++ b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js @@ -11,10 +11,10 @@ * Controller for handling permissions adding */ -export default ['$scope', '$rootScope', 'ProcessErrors', 'GetBasePath', -'SelectionInit', 'templateUrl', '$state', 'Rest', '$q', 'Wait', '$window', -function($scope, $rootScope, ProcessErrors, GetBasePath, - SelectionInit, templateUrl, $state, Rest, $q, Wait, $window) { +export default ['$scope', '$rootScope', 'ProcessErrors', 'GetBasePath', 'generateList', +'SelectionInit', 'templateUrl', '$state', 'Rest', '$q', 'Wait', '$window', 'QuerySet', 'UserList', +function($scope, $rootScope, ProcessErrors, GetBasePath, generateList, + SelectionInit, templateUrl, $state, Rest, $q, Wait, $window, qs, UserList) { $scope.$on("linkLists", function() { if ($state.current.name.split(".")[1] === "users") { @@ -26,16 +26,59 @@ function($scope, $rootScope, ProcessErrors, GetBasePath, init(); function init(){ - // search init - $scope.list = $scope.$parent.add_user_list; - $scope.add_user_dataset = $scope.$parent.add_user_dataset; - $scope.add_users = $scope.$parent.add_user_dataset.results; + $scope.add_user_default_params = { + order_by: 'username', + page_size: 5 + }; + + $scope.add_user_queryset = { + order_by: 'username', + page_size: 5 + }; + + let list = _.cloneDeep(UserList); + list.basePath = 'users'; + 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; + + // Fire off the initial search + qs.search(GetBasePath('users'), $scope.add_user_default_params) + .then(function(res) { + $scope.add_user_dataset = res.data; + $scope.add_users = $scope.add_user_dataset.results; + + let html = generateList.build({ + list: list, + mode: 'edit', + title: false + }); + + $scope.list = list; + + $scope.compileList(html); + + $scope.$watchCollection('add_users', function () { + if($scope.selectedItems) { + // Loop across the users and see if any of them should be "checked" + $scope.add_users.forEach(function(row, i) { + if (_.includes($scope.selectedItems, row.id)) { + $scope.add_users[i].isSelected = true; + } + }); + } + }); + + }); $scope.selectedItems = []; $scope.$on('selectedOrDeselected', function(e, value) { let item = value.value; - if (item.isSelected) { + if (value.isSelected) { $scope.selectedItems.push(item.id); } else { diff --git a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.directive.js b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.directive.js index f146149b13..65c721be17 100644 --- a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.directive.js +++ b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.directive.js @@ -7,7 +7,7 @@ /* jshint unused: vars */ import addUsers from './addUsers.controller'; export default - ['Wait', 'templateUrl', '$state', '$view', function(Wait, templateUrl, $state, $view) { + ['Wait', 'templateUrl', '$state', '$view', '$compile', function(Wait, templateUrl, $state, $view, $compile) { return { restrict: 'E', scope: { @@ -48,6 +48,10 @@ export default scope.closeModal(); }); + scope.compileList = function(html) { + $('#add-users-list').append($compile(html)(scope)); + }; + Wait('stop'); window.scrollTo(0,0); diff --git a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.partial.html b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.partial.html index a8a6cbbb81..b7e24e3940 100644 --- a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.partial.html +++ b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.partial.html @@ -15,8 +15,7 @@ -
-
+
-
+
-
+
From 80313779b28909b314c5a0daa1640ef243ef0115 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 6 Feb 2017 16:53:14 -0500 Subject: [PATCH 007/260] Fixed bug where deleting search tags was not removing the equivalent state param for lists in modals. Also did some cleanup on pagination directive. --- .../shared/paginate/paginate.controller.js | 36 ++++++++----- .../src/shared/paginate/paginate.partial.html | 20 +++---- .../smart-search/smart-search.controller.js | 54 ++++++++++++------- 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/awx/ui/client/src/shared/paginate/paginate.controller.js b/awx/ui/client/src/shared/paginate/paginate.controller.js index a4f45b7d5a..e4dbcbd22c 100644 --- a/awx/ui/client/src/shared/paginate/paginate.controller.js +++ b/awx/ui/client/src/shared/paginate/paginate.controller.js @@ -16,9 +16,21 @@ export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'Q $scope.pageSize = pageSize; function init() { - $scope.pageRange = calcPageRange($scope.current(), $scope.last()); - $scope.dataRange = calcDataRange(); + + let updatePaginationVariables = function() { + $scope.current = calcCurrent(); + $scope.last = calcLast(); + $scope.pageRange = calcPageRange($scope.current, $scope.last); + $scope.dataRange = calcDataRange(); + }; + + updatePaginationVariables(); + + $scope.$watch('collection', function(){ + updatePaginationVariables(); + }); } + $scope.dataCount = function() { return $filter('number')($scope.dataset.count); }; @@ -52,22 +64,22 @@ export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'Q $scope.dataset = res.data; $scope.collection = res.data.results; }); - $scope.pageRange = calcPageRange($scope.current(), $scope.last()); + $scope.pageRange = calcPageRange($scope.current, $scope.last); $scope.dataRange = calcDataRange(); }; - $scope.current = function() { + function calcLast() { + return Math.ceil($scope.dataset.count / pageSize); + } + + function calcCurrent() { if($scope.querySet) { return parseInt($scope.querySet.page || '1'); } else { return parseInt($stateParams[`${$scope.iterator}_search`].page || '1'); } - }; - - $scope.last = function() { - return Math.ceil($scope.dataset.count / pageSize); - }; + } function calcPageRange(current, last) { let result = []; @@ -84,12 +96,12 @@ export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'Q } function calcDataRange() { - if ($scope.current() === 1 && $scope.dataset.count < parseInt(pageSize)) { + if ($scope.current === 1 && $scope.dataset.count < parseInt(pageSize)) { return `1 - ${$scope.dataset.count}`; - } else if ($scope.current() === 1) { + } else if ($scope.current === 1) { return `1 - ${pageSize}`; } else { - let floor = (($scope.current() - 1) * parseInt(pageSize)) + 1; + let floor = (($scope.current - 1) * parseInt(pageSize)) + 1; let ceil = floor + parseInt(pageSize) < $scope.dataset.count ? floor + parseInt(pageSize) : $scope.dataset.count; return `${floor} - ${ceil}`; } diff --git a/awx/ui/client/src/shared/paginate/paginate.partial.html b/awx/ui/client/src/shared/paginate/paginate.partial.html index b00cc97fa1..8001e72da4 100644 --- a/awx/ui/client/src/shared/paginate/paginate.partial.html +++ b/awx/ui/client/src/shared/paginate/paginate.partial.html @@ -2,37 +2,37 @@
Page - {{current()}} of - {{last()}} + {{current}} of + {{last}}
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 22a91ec61a..6c10c1d752 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 @@ -148,9 +148,25 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' // remove tag, merge new queryset, $state.go $scope.remove = function(index) { - let tagToRemove = $scope.searchTags.splice(index, 1)[0]; - let termParts = SmartSearchService.splitTermIntoParts(tagToRemove); - let removed; + let tagToRemove = $scope.searchTags.splice(index, 1)[0], + termParts = SmartSearchService.splitTermIntoParts(tagToRemove), + removed; + + let removeFromQuerySet = function(set) { + _.each(removed, (value, key) => { + if (Array.isArray(set[key])){ + _.remove(set[key], (item) => item === value); + // If the array is now empty, remove that key + if(set[key].length === 0) { + delete set[key]; + } + } + else { + delete set[key]; + } + }); + }; + if (termParts.length === 1) { removed = setDefaults(tagToRemove); } @@ -169,21 +185,16 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' } removed = qs.encodeParam(encodeParams); } - _.each(removed, (value, key) => { - if (Array.isArray(queryset[key])){ - _.remove(queryset[key], (item) => item === value); - // If the array is now empty, remove that key - if(queryset[key].length === 0) { - delete queryset[key]; - } - } - else { - delete queryset[key]; - } - }); + removeFromQuerySet(queryset); if(!$scope.querySet) { $state.go('.', { - [$scope.iterator + '_search']: queryset }, {notify: false}); + [$scope.iterator + '_search']: queryset }, {notify: false}).then(function(){ + // ISSUE: for some reason deleting a tag from a list in a modal does not + // remove the param from $stateParams. Here we'll manually check to make sure + // that that happened and remove it if it didn't. + + removeFromQuerySet($stateParams[`${$scope.iterator}_search`]); + }); } qs.search(path, queryset).then((res) => { if($scope.querySet) { @@ -243,7 +254,6 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' } }); - params.page = '1'; queryset = _.merge(queryset, params, (objectValue, sourceValue, key, object) => { if (object[key] && object[key] !== sourceValue){ if(_.isArray(object[key])) { @@ -262,12 +272,20 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' return undefined; } }); + + // Go back to the first page after a new search + delete queryset.page; + // https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic // This transition will not reload controllers/resolves/views // but will register new $stateParams[$scope.iterator + '_search'] terms if(!$scope.querySet) { $state.go('.', { - [$scope.iterator + '_search']: queryset }, {notify: false}); + [$scope.iterator + '_search']: queryset }, {notify: false}).then(function(){ + // ISSUE: same as above in $scope.remove. For some reason deleting the page + // from the queryset works for all lists except lists in modals. + delete $stateParams[$scope.iterator + '_search'].page; + }); } qs.search(path, queryset).then((res) => { if($scope.querySet) { From bfe31035424a42b8841bb791a34cf72332d6f645 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Fri, 3 Feb 2017 16:52:48 -0800 Subject: [PATCH 008/260] fix for #5181, about entering invalid search entries. --- awx/ui/client/src/shared/smart-search/queryset.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 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 877222da11..869d4e57a0 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -264,11 +264,11 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear this.error(response.data, response.status); - return response; + throw response; }.bind(this)); }, error(data, status) { - ProcessErrors($rootScope, data, status, null, { + ProcessErrors($rootScope, null, status, null, { hdr: 'Error!', msg: 'Call to ' + this.url + '. GET returned: ' + status }); From f00214495ad59185a76440634a78ed1a98f6057b Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Fri, 3 Feb 2017 17:20:30 -0800 Subject: [PATCH 009/260] fix for #5179 for deleting tags for non search params --- .../shared/smart-search/smart-search.controller.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 22a91ec61a..e16485550f 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 @@ -177,6 +177,18 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' delete queryset[key]; } } + if(queryset.search && queryset.search){ + if (Array.isArray(queryset.search)){ + _.remove(queryset.search, (item) => item.indexOf(value) > -1); + // If the array is now empty, remove that key + if(queryset.search.length === 0) { + delete queryset.search; + } + } + else if(queryset.search.indexOf(key) > -1){ + delete queryset.search; + } + } else { delete queryset[key]; } From ab57396fcf77bfefee2a16ddac0025fcdb28670f Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 6 Feb 2017 10:53:42 -0800 Subject: [PATCH 010/260] Generalizing error message when user inputs an invalid search --- awx/ui/client/src/shared/smart-search/queryset.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 869d4e57a0..10ae7ef3c8 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -270,7 +270,7 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear error(data, status) { ProcessErrors($rootScope, null, status, null, { hdr: 'Error!', - msg: 'Call to ' + this.url + '. GET returned: ' + status + msg: "Invalid search term entered." }); } }; From 99b7532ef6a237f3942b5698f5394aca28531711 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 7 Feb 2017 17:43:43 -0500 Subject: [PATCH 011/260] switcharoo of team admin for member role --- awx/api/serializers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index cdb30e113d..426684fd1f 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1622,8 +1622,11 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['user_capabilities'] = {'unattach': False} return { 'role': role_dict, 'descendant_roles': get_roles_on_resource(obj, role)} - def format_team_role_perm(team_role, permissive_role_ids): + def format_team_role_perm(naive_team_role, permissive_role_ids): ret = [] + team_role = naive_team_role + if naive_team_role.role_field == 'admin_role': + team_role = naive_team_role.content_object.member_role for role in team_role.children.filter(id__in=permissive_role_ids).all(): role_dict = { 'id': role.id, @@ -1682,11 +1685,11 @@ class ResourceAccessListElementSerializer(UserSerializer): ret['summary_fields']['direct_access'] \ = [format_role_perm(r) for r in direct_access_roles.distinct()] \ - + [y for x in (format_team_role_perm(r, direct_permissive_role_ids) for r in direct_team_roles.distinct()) for y in x] + + [y for x in (format_team_role_perm(r, direct_permissive_role_ids) for r in direct_team_roles.distinct()) for y in x] \ + + [y for x in (format_team_role_perm(r, all_permissive_role_ids) for r in indirect_team_roles.distinct()) for y in x] ret['summary_fields']['indirect_access'] \ - = [format_role_perm(r) for r in indirect_access_roles.distinct()] \ - + [y for x in (format_team_role_perm(r, all_permissive_role_ids) for r in indirect_team_roles.distinct()) for y in x] + = [format_role_perm(r) for r in indirect_access_roles.distinct()] return ret From 24c681d47c5699b4310063a2f3810e27ac655987 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Wed, 8 Feb 2017 09:44:55 -0500 Subject: [PATCH 012/260] Search bar width, action button placement --- awx/ui/client/legacy-styles/ansible-ui.less | 10 ++++- awx/ui/client/legacy-styles/lists.less | 9 ++++ .../list/organizations-list.partial.html | 21 +++++---- awx/ui/client/src/shared/form-generator.js | 12 +++--- .../list-generator/list-generator.factory.js | 43 +++++++++---------- .../smart-search/smart-search.block.less | 13 +++++- 6 files changed, 67 insertions(+), 41 deletions(-) diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index c5f2e91933..825576f8c8 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -921,7 +921,7 @@ input[type="checkbox"].checkbox-no-label { /* Display list actions next to search widget */ .list-actions { text-align: right; - margin-bottom: 20px; + margin-bottom: -34px; .fa-lg { vertical-align: -8%; @@ -1939,10 +1939,16 @@ tr td button i { padding-right: 15px; } + + +} + +// lists.less uses 600px as the breakpoint, doing same for consistency +@media (max-width: 600px) { .list-actions { text-align: left; + margin-bottom: 20px; } - } .nvtooltip { diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index eb0a50529b..4aa4f560c9 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -153,10 +153,13 @@ table, tbody { .List-actionHolder { justify-content: flex-end; display: flex; + // margin-bottom: 20px; + // float: right; } .List-actions { display: flex; + margin-bottom: -32px; } .List-auxAction { @@ -419,7 +422,13 @@ table, tbody { flex: 1 0 auto; margin-top: 12px; } + .List-actions { + margin-bottom: 20px; + } .List-well { margin-top: 20px; } + .List-action:not(.ng-hide) ~ .List-action:not(.ng-hide) { + margin-left: 0; + } } diff --git a/awx/ui/client/src/organizations/list/organizations-list.partial.html b/awx/ui/client/src/organizations/list/organizations-list.partial.html index 9531107134..be2827632a 100644 --- a/awx/ui/client/src/organizations/list/organizations-list.partial.html +++ b/awx/ui/client/src/organizations/list/organizations-list.partial.html @@ -10,18 +10,21 @@ {{ orgCount }}
-
- - -
+
+
+
+ + +
+
+ +
From fa9d47f54850032fe24245f2700abcd8e141a048 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 8 Feb 2017 19:01:24 -0500 Subject: [PATCH 025/260] Override the ngHref's for jt/wfjt lists inside of the rbac multiselect --- .../access/rbac-multiselect/rbac-multiselect-list.directive.js | 2 ++ 1 file changed, 2 insertions(+) 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 f339e0e579..aebe4c94bb 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 @@ -65,6 +65,7 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL description: list.fields.description }; list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11'; + list.fields.name.ngHref = '#/templates/job_template/{{job_template.id}}'; list.fields.description.columnClass = 'col-md-5 col-sm-5 hidden-xs'; break; @@ -77,6 +78,7 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL description: list.fields.description }; list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11'; + list.fields.name.ngHref = '#/templates/workflow_job_template/{{workflow_template.id}}'; list.fields.description.columnClass = 'col-md-5 col-sm-5 hidden-xs'; break; case 'Users': From 57883e37bf68a84d35a946484dcbd021b219b110 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 8 Feb 2017 16:36:53 -0800 Subject: [PATCH 026/260] Adding default params for some lookup modals the organization and inventory-script lookup modals require some additional default params set to properly present the user w/ options they have permission to see. If these default params aren't set, they'll show up as search tags, which we don't want. --- .../src/shared/stateDefinitions.factory.js | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index f6f0bf1950..f24ec4993d 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -646,6 +646,32 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto generateLookupNodes: function(form, formStateDefinition) { function buildFieldDefinition(field) { + + // Some lookup modals require some additional default params, + // namely organization and inventory_script. If these params + // aren't set as default params out of the gate, then smart + // search will think they need to be set as search tags. + var params; + if(field.sourceModel === "organization"){ + params = { + page_size: '5', + role_level: 'admin_role' + }; + } + else if(field.sourceModel === "inventory_script"){ + params = { + page_size: '5', + role_level: 'admin_role', + organization: null + }; + } + else { + params = { + page_size: '5', + role_level: 'use_role' + }; + } + let state = $stateExtender.buildDefinition({ searchPrefix: field.sourceModel, //squashSearchUrl: true, @issue enable @@ -658,10 +684,7 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto }, params: { [field.sourceModel + '_search']: { - value: { - page_size: '5', - role_level: 'use_role' - } + value: params } }, ncyBreadcrumb: { From 18cd38580bfdd6902004a12b73267be007b51f6c Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 8 Feb 2017 17:08:55 -0800 Subject: [PATCH 027/260] fixing the html generation of toggles on related tabs for notifications --- awx/ui/client/src/shared/generator-helpers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index 7942c02163..6d3788abab 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -507,10 +507,10 @@ angular.module('GeneratorHelpers', [systemStatus.name]) } else if (field.type === 'toggle') { html += "
+
`; } @@ -1857,7 +1857,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat ${actionButtons}
`; } - + // smart-search directive html += `
Date: Thu, 9 Feb 2017 17:16:56 -0500 Subject: [PATCH 049/260] modularize logging config in proper Django fashion --- awx/conf/apps.py | 10 ++---- awx/main/tasks.py | 25 +++---------- awx/main/utils/handlers.py | 72 +++++++++++++++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 33 deletions(-) diff --git a/awx/conf/apps.py b/awx/conf/apps.py index 9ae459fb35..a70d21326c 100644 --- a/awx/conf/apps.py +++ b/awx/conf/apps.py @@ -2,7 +2,7 @@ from django.apps import AppConfig # from django.core import checks from django.utils.translation import ugettext_lazy as _ -from django.utils.log import configure_logging +from awx.main.utils.handlers import configure_external_logger from django.conf import settings @@ -15,10 +15,4 @@ class ConfConfig(AppConfig): self.module.autodiscover() from .settings import SettingsWrapper SettingsWrapper.initialize() - if settings.LOG_AGGREGATOR_ENABLED: - LOGGING_DICT = settings.LOGGING - LOGGING_DICT['handlers']['http_receiver']['class'] = 'awx.main.utils.handlers.HTTPSHandler' - if 'awx' in settings.LOG_AGGREGATOR_LOGGERS: - if 'http_receiver' not in LOGGING_DICT['loggers']['awx']['handlers']: - LOGGING_DICT['loggers']['awx']['handlers'] += ['http_receiver'] - configure_logging(settings.LOGGING_CONFIG, LOGGING_DICT) + configure_external_logger(settings) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 0cd0dcf6c8..08242cac7c 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -32,7 +32,7 @@ import pexpect # Celery from celery import Task, task -from celery.signals import celeryd_init, worker_ready +from celery.signals import celeryd_init, worker_process_init from celery import current_app # Django @@ -54,6 +54,7 @@ from awx.main.task_engine import TaskEnhancer from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url, check_proot_installed, build_proot_temp_dir, wrap_args_with_proot, get_system_task_capacity, OutputEventFilter, parse_yaml_or_json) +from awx.main.utils.handlers import configure_external_logger from awx.main.consumers import emit_channel_notification __all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate', @@ -86,26 +87,10 @@ def celery_startup(conf=None, **kwargs): logger.error("Failed to rebuild schedule {}: {}".format(sch, e)) -def _setup_tower_logger(): - global logger - from django.utils.log import configure_logging - LOGGING_DICT = settings.LOGGING - if settings.LOG_AGGREGATOR_ENABLED: - LOGGING_DICT['handlers']['http_receiver']['class'] = 'awx.main.utils.handlers.HTTPSHandler' - LOGGING_DICT['handlers']['http_receiver']['async'] = False - if 'awx' in settings.LOG_AGGREGATOR_LOGGERS: - if 'http_receiver' not in LOGGING_DICT['loggers']['awx']['handlers']: - LOGGING_DICT['loggers']['awx']['handlers'] += ['http_receiver'] - configure_logging(settings.LOGGING_CONFIG, LOGGING_DICT) - logger = logging.getLogger('awx.main.tasks') - - -@worker_ready.connect +@worker_process_init.connect def task_set_logger_pre_run(*args, **kwargs): cache.close() - if settings.LOG_AGGREGATOR_ENABLED: - _setup_tower_logger() - logger.debug('Custom Tower logger configured for worker process.') + configure_external_logger(settings, is_startup=False) def _uwsgi_reload(): @@ -121,7 +106,7 @@ def _uwsgi_reload(): def _reset_celery_logging(): - # Worker logger reloaded, now send signal to restart pool + # Send signal to restart thread pool app = current_app._get_current_object() app.control.broadcast('pool_restart', arguments={'reload': True}, destination=['celery@{}'.format(settings.CLUSTER_HOST_ID)], reply=False) diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index 69f556ddba..1a667d2ff5 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -12,8 +12,11 @@ import traceback from requests_futures.sessions import FuturesSession -# custom -from django.conf import settings as django_settings +# AWX +from awx.main.utils.formatters import LogstashFormatter + + +__all__ = ['HTTPSNullHandler', 'BaseHTTPSHandler', 'HTTPSHandler', 'configure_external_logger'] # AWX external logging handler, generally designed to be used # with the accompanying LogstashHandler, derives from python-logstash library @@ -40,7 +43,7 @@ def unused_callback(sess, resp): class HTTPSNullHandler(logging.NullHandler): "Placeholder null handler to allow loading without database access" - def __init__(self, host, **kwargs): + def __init__(self, *args, **kwargs): return super(HTTPSNullHandler, self).__init__() @@ -167,5 +170,64 @@ class BaseHTTPSHandler(logging.Handler): class HTTPSHandler(object): - def __new__(cls, *args, **kwargs): - return BaseHTTPSHandler.from_django_settings(django_settings, *args, **kwargs) + def __new__(cls, settings_module, *args, **kwargs): + return BaseHTTPSHandler.from_django_settings(settings_module, *args, **kwargs) + + +def add_or_remove_logger(address, instance, adding=True): + specific_logger = logging.getLogger(address) + i_occurance = None + for i in range(len(specific_logger.handlers)): + if isinstance(specific_logger.handlers[i], (HTTPSNullHandler, BaseHTTPSHandler)): + i_occurance = i + break + + if i_occurance is None and not adding: + return + elif i_occurance is None: + specific_logger.handlers.append(instance) + else: + specific_logger.handlers[i_occurance] = instance + + +def configure_external_logger(settings_module, async_flag=True, is_startup=True): + + is_enabled = settings_module.LOG_AGGREGATOR_ENABLED + if is_startup and (not is_enabled): + # Pass-through if external logging not being used + return + + if is_enabled: + instance = HTTPSHandler(settings_module, async=async_flag) + instance.setFormatter(LogstashFormatter()) + else: + instance = HTTPSNullHandler() + + add_or_remove_logger('awx.analytics', instance, adding=is_enabled) + add_or_remove_logger('awx', instance, adding=(is_enabled and 'awx' in settings_module.LOG_AGGREGATOR_LOGGERS)) + + +def configure_external_logger_old(settings_module, async_flag=True, is_startup=True): + + from django.utils.log import configure_logging + + is_enabled = settings_module.LOG_AGGREGATOR_ENABLED + if is_startup and (not is_enabled): + # Pass-through if external logging not being used + return + + LOGGING_DICT = settings_module.LOGGING + if is_enabled: + LOGGING_DICT['handlers']['http_receiver']['class'] = 'awx.main.utils.handlers.BaseHTTPSHandler' + if not async_flag: + LOGGING_DICT['handlers']['http_receiver']['async'] = False + else: + LOGGING_DICT['handlers']['http_receiver']['async'] = True + for param, django_setting_name in PARAM_NAMES.items(): + LOGGING_DICT['handlers']['http_receiver'][param] = getattr(settings_module, django_setting_name, None) + if 'awx' in settings_module.LOG_AGGREGATOR_LOGGERS: + if 'http_receiver' not in LOGGING_DICT['loggers']['awx']['handlers']: + LOGGING_DICT['loggers']['awx']['handlers'] += ['http_receiver'] + # External logging not enabled, but removal of existing configuration needed + configure_logging(settings_module.LOGGING_CONFIG, LOGGING_DICT) + From cdf28f1bca59d87bca1eaf1e339969f4196a3de3 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 10 Feb 2017 11:37:50 -0500 Subject: [PATCH 050/260] remove old logger reconfig method --- awx/main/tasks.py | 2 +- awx/main/utils/handlers.py | 28 +--------------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 08242cac7c..d3d12b6259 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -90,7 +90,7 @@ def celery_startup(conf=None, **kwargs): @worker_process_init.connect def task_set_logger_pre_run(*args, **kwargs): cache.close() - configure_external_logger(settings, is_startup=False) + configure_external_logger(settings, async_flag=False, is_startup=False) def _uwsgi_reload(): diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index 1a667d2ff5..08005f7ee4 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -181,7 +181,7 @@ def add_or_remove_logger(address, instance, adding=True): if isinstance(specific_logger.handlers[i], (HTTPSNullHandler, BaseHTTPSHandler)): i_occurance = i break - + if i_occurance is None and not adding: return elif i_occurance is None: @@ -205,29 +205,3 @@ def configure_external_logger(settings_module, async_flag=True, is_startup=True) add_or_remove_logger('awx.analytics', instance, adding=is_enabled) add_or_remove_logger('awx', instance, adding=(is_enabled and 'awx' in settings_module.LOG_AGGREGATOR_LOGGERS)) - - -def configure_external_logger_old(settings_module, async_flag=True, is_startup=True): - - from django.utils.log import configure_logging - - is_enabled = settings_module.LOG_AGGREGATOR_ENABLED - if is_startup and (not is_enabled): - # Pass-through if external logging not being used - return - - LOGGING_DICT = settings_module.LOGGING - if is_enabled: - LOGGING_DICT['handlers']['http_receiver']['class'] = 'awx.main.utils.handlers.BaseHTTPSHandler' - if not async_flag: - LOGGING_DICT['handlers']['http_receiver']['async'] = False - else: - LOGGING_DICT['handlers']['http_receiver']['async'] = True - for param, django_setting_name in PARAM_NAMES.items(): - LOGGING_DICT['handlers']['http_receiver'][param] = getattr(settings_module, django_setting_name, None) - if 'awx' in settings_module.LOG_AGGREGATOR_LOGGERS: - if 'http_receiver' not in LOGGING_DICT['loggers']['awx']['handlers']: - LOGGING_DICT['loggers']['awx']['handlers'] += ['http_receiver'] - # External logging not enabled, but removal of existing configuration needed - configure_logging(settings_module.LOGGING_CONFIG, LOGGING_DICT) - From 9c686f680de0319fe1ce813f9cb2548849e800e4 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Fri, 10 Feb 2017 13:26:36 -0500 Subject: [PATCH 051/260] Workflow editor/details graph responsiveness --- awx/ui/client/src/shared/Modal.js | 2 +- .../workflow-chart.directive.js | 1194 +++++++++-------- .../workflow-maker.controller.js | 4 + .../workflow-maker.directive.js | 37 +- .../workflow-maker.partial.html | 2 +- 5 files changed, 680 insertions(+), 559 deletions(-) diff --git a/awx/ui/client/src/shared/Modal.js b/awx/ui/client/src/shared/Modal.js index e37defa900..e6890ac367 100644 --- a/awx/ui/client/src/shared/Modal.js +++ b/awx/ui/client/src/shared/Modal.js @@ -46,7 +46,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper']) return function(params) { - var scope = params.scope, + let scope = params.scope, buttonSet = params.buttons, width = params.width || 500, height = params.height || 600, 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 642dc26971..3623de0100 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','moment', '$timeout', '$window', + function($state, moment, $timeout, $window) { return { scope: { @@ -21,38 +21,91 @@ export default [ '$state','moment', link: function(scope, element) { let margin = {top: 20, right: 20, bottom: 20, left: 20}, - width = 950, - height = 550, i = 0, nodeW = 120, nodeH = 60, rootW = 60, - rootH = 40; + rootH = 40, + startNodeOffsetY = scope.mode === 'details' ? 17 : 10, + verticalSpaceBetweenNodes = 20, + windowHeight, + windowWidth, + tree, + line, + zoomObj, + baseSvg, + svgGroup; - let tree = d3.layout.tree() - .size([height, width]); + scope.dimensionsSet = false; - let line = d3.svg.line() - .x(function(d){return d.x;}) - .y(function(d){return d.y;}); + $timeout(function(){ + let dimensions = calcAvailableScreenSpace(); - let zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]); + windowHeight = dimensions.height; + windowWidth = dimensions.width; - let baseSvg = d3.select(element[0]).append("svg") - .attr("width", width - margin.right - margin.left) - .attr("height", height - margin.top - margin.bottom) - .attr("class", "WorkflowChart-svg") - .call(zoomObj - .on("zoom", naturalZoom) - ); + $('.WorkflowMaker-chart').css("height", windowHeight); + $('.WorkflowMaker-chart').css("width", windowWidth); - let svgGroup = baseSvg.append("g") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + scope.dimensionsSet = true; + + init(); + }); + + function init() { + tree = d3.layout.tree() + .nodeSize([nodeH + verticalSpaceBetweenNodes,nodeW]) + .separation(function(a, b) { + // This should tighten up some of the other nodes so there's not so much wasted space + return a.parent === b.parent ? 1 : 1.25; + }); + + line = d3.svg.line() + .x(function(d){return d.x;}) + .y(function(d){return d.y;}); + + zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]); + + baseSvg = d3.select(element[0]).append("svg") + .attr("class", "WorkflowChart-svg") + .call(zoomObj + .on("zoom", naturalZoom) + ); + + svgGroup = baseSvg.append("g") + .attr("transform", "translate(" + margin.left + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")"); + } + + function calcAvailableScreenSpace() { + let dimensions = {}; + + if(scope.mode !== 'details') { + // This is the workflow editor + dimensions.height = $('.WorkflowMaker-contentLeft').outerHeight() - $('.WorkflowLegend-maker').outerHeight(); + dimensions.width = $('#workflow-modal-dialog').width() - $('.WorkflowMaker-contentRight').outerWidth(); + + console.log(dimensions); + } + else { + // This is the workflow details view + let panel = $('.WorkflowResults-rightSide').children('.Panel')[0]; + let panelWidth = $(panel).width(); + let panelHeight = $(panel).height(); + let headerHeight = $('.StandardOut-panelHeader').outerHeight(); + let legendHeight = $('.WorkflowLegend-details').outerHeight(); + let proposedHeight = panelHeight - headerHeight - legendHeight - 40; + + dimensions.height = proposedHeight > 200 ? proposedHeight : 200; + dimensions.width = panelWidth; + } + + return dimensions; + } function lineData(d){ let sourceX = d.source.isStartNode ? d.source.y + rootW : d.source.y + nodeW; - let sourceY = d.source.isStartNode ? d.source.x + 10 + rootH / 2 : d.source.x + nodeH / 2; + let sourceY = d.source.isStartNode ? d.source.x + startNodeOffsetY + rootH / 2 : d.source.x + nodeH / 2; let targetX = d.target.y; let targetY = d.target.x + nodeH / 2; @@ -108,7 +161,7 @@ export default [ '$state','moment', let scale = d3.event.scale, translation = d3.event.translate; - translation = [translation[0] + (margin.left*scale), translation[1] + (margin.top*scale)]; + translation = [translation[0] + (margin.left*scale), translation[1] + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)]; svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")"); @@ -122,12 +175,12 @@ export default [ '$state','moment', 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; + unscaledOffsetX = (translation[0] + ((windowWidth*origZoom) - windowWidth)/2)/origZoom, + unscaledOffsetY = (translation[1] + ((windowHeight*origZoom) - windowHeight)/2)/origZoom, + translateX = unscaledOffsetX*scale - ((scale*windowWidth)-windowWidth)/2, + translateY = unscaledOffsetY*scale - ((scale*windowHeight)-windowHeight)/2; - svgGroup.attr("transform", "translate(" + [translateX + (margin.left*scale), translateY + (margin.top*scale)] + ")scale(" + scale + ")"); + svgGroup.attr("transform", "translate(" + [translateX + (margin.left*scale), translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)] + ")scale(" + scale + ")"); zoomObj.scale(scale); zoomObj.translate([translateX, translateY]); } @@ -145,572 +198,584 @@ export default [ '$state','moment', translateX = translateCoords[0]; translateY = direction === 'up' ? translateCoords[1] - distance : translateCoords[1] + distance; } - svgGroup.attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")"); + svgGroup.attr("transform", "translate(" + translateX + "," + (translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)) + ")scale(" + scale + ")"); zoomObj.translate([translateX, translateY]); } function resetZoomAndPan() { - svgGroup.attr("transform", "translate(" + margin.left + "," + margin.top + ")scale(" + 1 + ")"); + svgGroup.attr("transform", "translate(" + margin.left + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")scale(" + 1 + ")"); // Update the zoomObj zoomObj.scale(1); zoomObj.translate([0,0]); } function update() { - // Declare the nodes - let nodes = tree.nodes(scope.treeData), - links = tree.links(nodes); - let node = svgGroup.selectAll("g.node") - .data(nodes, function(d) { - d.y = d.depth * 180; - return d.id || (d.id = ++i); - }); + if(scope.dimensionsSet) { + // Declare the nodes + let nodes = tree.nodes(scope.treeData), + links = tree.links(nodes); + let node = svgGroup.selectAll("g.node") + .data(nodes, function(d) { + d.y = d.depth * 180; + return d.id || (d.id = ++i); + }); - let nodeEnter = node.enter().append("g") - .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 + ")"; }); + let nodeEnter = node.enter().append("g") + .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 + ")"; }); - nodeEnter.each(function(d) { - let thisNode = d3.select(this); - if(d.isStartNode && scope.mode === 'details') { - // Overwrite the default root height and width and replace it with a small blue square - rootW = 25; - rootH = 25; - thisNode.append("rect") - .attr("width", rootW) - .attr("height", rootH) - .attr("y", 10) - .attr("rx", 5) - .attr("ry", 5) - .attr("fill", "#337ab7") - .attr("class", "WorkflowChart-rootNode"); - } - else if(d.isStartNode && scope.mode !== 'details') { - thisNode.append("rect") - .attr("width", rootW) - .attr("height", rootH) - .attr("y", 10) - .attr("rx", 5) - .attr("ry", 5) - .attr("fill", "#5cb85c") - .attr("class", "WorkflowChart-rootNode") - .call(add_node); - thisNode.append("text") - .attr("x", 13) - .attr("y", 30) - .attr("dy", ".35em") - .attr("class", "WorkflowChart-startText") - .text(function () { return "START"; }) - .call(add_node); - } - else { - thisNode.append("rect") - .attr("width", nodeW) - .attr("height", nodeH) - .attr("rx", 5) - .attr("ry", 5) - .attr('stroke', function(d) { - if(d.job && d.job.status) { - if(d.job.status === "successful"){ - return "#5cb85c"; - } - else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") { - return "#d9534f"; + nodeEnter.each(function(d) { + let thisNode = d3.select(this); + if(d.isStartNode && scope.mode === 'details') { + // Overwrite the default root height and width and replace it with a small blue square + rootW = 25; + rootH = 25; + thisNode.append("rect") + .attr("width", rootW) + .attr("height", rootH) + .attr("y", startNodeOffsetY) + .attr("rx", 5) + .attr("ry", 5) + .attr("fill", "#337ab7") + .attr("class", "WorkflowChart-rootNode"); + } + else if(d.isStartNode && scope.mode !== 'details') { + thisNode.append("rect") + .attr("width", rootW) + .attr("height", rootH) + //.attr("y", (windowHeight-margin.top-margin.bottom)/2 - rootH) + .attr("y", 10) + .attr("rx", 5) + .attr("ry", 5) + .attr("fill", "#5cb85c") + .attr("class", "WorkflowChart-rootNode") + .call(add_node); + thisNode.append("text") + .attr("x", 13) + //.attr("y", (windowHeight-margin.top-margin.bottom)/2 - rootH + rootH/2) + .attr("y", 30) + .attr("dy", ".35em") + .attr("class", "WorkflowChart-startText") + .text(function () { return "START"; }) + .call(add_node); + } + else { + thisNode.append("rect") + .attr("width", nodeW) + .attr("height", nodeH) + .attr("rx", 5) + .attr("ry", 5) + .attr('stroke', function(d) { + if(d.job && d.job.status) { + if(d.job.status === "successful"){ + return "#5cb85c"; + } + else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") { + return "#d9534f"; + } + else { + return "#D7D7D7"; + } } 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, nodeH, 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.status) ? 20 : nodeW / 2; }) + .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 2; }) + .attr("dy", ".35em") + .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 : ""; + }).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) + .attr("dy", ".35em") + .attr("text-anchor", "middle") + .attr("class", "WorkflowChart-defaultText WorkflowChart-incompleteText") + .html(function () { + return "\uf06a INCOMPLETE"; + }) + .style("display", function(d) { return d.unifiedJobTemplate || d.placeholder ? "none" : null; }); + + thisNode.append("circle") + .attr("cy", nodeH) + .attr("r", 10) + .attr("class", "WorkflowChart-nodeTypeCircle") + .style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; }); + + thisNode.append("text") + .attr("y", nodeH) + .attr("dy", ".35em") + .attr("text-anchor", "middle") + .attr("class", "WorkflowChart-nodeTypeLetter") + .text(function (d) { + return (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update")) ? "P" : (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? "I" : ""); + }) + .style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; }); + + thisNode.append("rect") + .attr("width", nodeW) + .attr("height", nodeH) + .attr("class", "transparentRect") + .call(edit_node) + .on("mouseover", function(d) { + if(!d.isStartNode) { + d3.select("#node-" + d.id) + .classed("hovering", true); + } + }) + .on("mouseout", function(d){ + if(!d.isStartNode) { + d3.select("#node-" + d.id) + .classed("hovering", false); + } + }); + thisNode.append("text") + .attr("x", nodeW - 50) + .attr("y", nodeH - 10) + .attr("dy", ".35em") + .attr("class", "WorkflowChart-detailsLink") + .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }) + .text(function () { + return "DETAILS"; + }) + .call(details); + thisNode.append("circle") + .attr("id", function(d){return "node-" + d.id + "-add";}) + .attr("cx", nodeW) + .attr("r", 10) + .attr("class", "addCircle nodeCircle") + .style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; }) + .call(add_node) + .on("mouseover", function(d) { + d3.select("#node-" + d.id) + .classed("hovering", true); + d3.select("#node-" + d.id + "-add") + .classed("addHovering", true); + }) + .on("mouseout", function(d){ + d3.select("#node-" + d.id) + .classed("hovering", false); + d3.select("#node-" + d.id + "-add") + .classed("addHovering", false); + }); + thisNode.append("path") + .attr("class", "nodeAddCross WorkflowChart-hoverPath") + .style("fill", "white") + .attr("transform", function() { return "translate(" + nodeW + "," + 0 + ")"; }) + .attr("d", d3.svg.symbol() + .size(60) + .type("cross") + ) + .style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; }) + .call(add_node) + .on("mouseover", function(d) { + d3.select("#node-" + d.id) + .classed("hovering", true); + d3.select("#node-" + d.id + "-add") + .classed("addHovering", true); + }) + .on("mouseout", function(d){ + d3.select("#node-" + d.id) + .classed("hovering", false); + d3.select("#node-" + d.id + "-add") + .classed("addHovering", false); + }); + thisNode.append("circle") + .attr("id", function(d){return "node-" + d.id + "-remove";}) + .attr("cx", nodeW) + .attr("cy", nodeH) + .attr("r", 10) + .attr("class", "removeCircle") + .style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) + .call(remove_node) + .on("mouseover", function(d) { + d3.select("#node-" + d.id) + .classed("hovering", true); + d3.select("#node-" + d.id + "-remove") + .classed("removeHovering", true); + }) + .on("mouseout", function(d){ + d3.select("#node-" + d.id) + .classed("hovering", false); + d3.select("#node-" + d.id + "-remove") + .classed("removeHovering", false); + }); + thisNode.append("path") + .attr("class", "nodeRemoveCross WorkflowChart-hoverPath") + .style("fill", "white") + .attr("transform", function() { return "translate(" + nodeW + "," + nodeH + ") rotate(-45)"; }) + .attr("d", d3.svg.symbol() + .size(60) + .type("cross") + ) + .style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) + .call(remove_node) + .on("mouseover", function(d) { + d3.select("#node-" + d.id) + .classed("hovering", true); + d3.select("#node-" + d.id + "-remove") + .classed("removeHovering", true); + }) + .on("mouseout", function(d){ + d3.select("#node-" + d.id) + .classed("hovering", false); + d3.select("#node-" + d.id + "-remove") + .classed("removeHovering", false); + }); + + thisNode.append("circle") + .attr("class", function(d) { + + let statusClass = "WorkflowChart-nodeStatus "; + + if(d.job){ + switch(d.job.status) { + case "pending": + statusClass += "workflowChart-nodeStatus--running"; + break; + case "waiting": + statusClass += "workflowChart-nodeStatus--running"; + break; + case "running": + statusClass += "workflowChart-nodeStatus--running"; + break; + case "successful": + statusClass += "workflowChart-nodeStatus--success"; + break; + 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.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"; }); + } + }); + + node.exit().remove(); + + let link = svgGroup.selectAll("g.link") + .data(links, function(d) { + return d.source.id + "-" + d.target.id; + }); + + let linkEnter = link.enter().append("g") + .attr("class", "link") + .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;}); + + // Add entering links in the parent’s old position. + linkEnter.insert("path", "g") + .attr("class", function(d) { + return (d.source.placeholder || d.target.placeholder) ? "linkPath placeholder" : "linkPath"; + }) + .attr("d", lineData) + .attr('stroke', function(d) { + if(d.target.edgeType) { + if(d.target.edgeType === "failure") { + return "#d9534f"; + } + else if(d.target.edgeType === "success") { + return "#5cb85c"; + } + else if(d.target.edgeType === "always"){ + return "#337ab7"; + } + } + else { + return "#D7D7D7"; + } + }); + + linkEnter.append("circle") + .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";}) + .attr("cx", function(d) { + return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2; + }) + .attr("cy", function(d) { + return (d.source.isStartNode) ? ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2; + }) + .attr("r", 10) + .attr("class", "addCircle linkCircle") + .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) + .call(add_node_between) + .on("mouseover", function(d) { + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("hovering", true); + d3.select("#link-" + d.source.id + "-" + d.target.id + "-add") + .classed("addHovering", true); + }) + .on("mouseout", function(d){ + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("hovering", false); + d3.select("#link-" + d.source.id + "-" + d.target.id + "-add") + .classed("addHovering", false); + }); + + linkEnter.append("path") + .attr("class", "linkCross") + .style("fill", "white") + .attr("transform", function(d) { + let translate; + if(d.source.isStartNode) { + translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")"; + } + else { + translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")"; + } + return translate; + }) + .attr("d", d3.svg.symbol() + .size(60) + .type("cross") + ) + .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) + .call(add_node_between) + .on("mouseover", function(d) { + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("hovering", true); + d3.select("#link-" + d.source.id + "-" + d.target.id + "-add") + .classed("addHovering", true); + }) + .on("mouseout", function(d){ + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("hovering", false); + d3.select("#link-" + d.source.id + "-" + d.target.id + "-add") + .classed("addHovering", false); + }); + + link.exit().remove(); + + // Transition nodes and links to their new positions. + let t = baseSvg.transition(); + + t.selectAll(".nodeCircle") + .style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; }); + + t.selectAll(".nodeAddCross") + .style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; }); + + t.selectAll(".removeCircle") + .style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }); + + t.selectAll(".nodeRemoveCross") + .style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }); + + t.selectAll(".linkPath") + .attr("class", function(d) { + return (d.source.placeholder || d.target.placeholder) ? "linkPath placeholder" : "linkPath"; + }) + .attr("d", lineData) + .attr('stroke', function(d) { + if(d.target.edgeType) { + if(d.target.edgeType === "failure") { + return "#d9534f"; + } + else if(d.target.edgeType === "success") { + return "#5cb85c"; + } + else if(d.target.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, nodeH, 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.status) ? 20 : nodeW / 2; }) - .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 2; }) - .attr("dy", ".35em") - .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 : ""; - }).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) - .attr("dy", ".35em") - .attr("text-anchor", "middle") - .attr("class", "WorkflowChart-defaultText WorkflowChart-incompleteText") - .html(function () { - return "\uf06a INCOMPLETE"; - }) - .style("display", function(d) { return d.unifiedJobTemplate || d.placeholder ? "none" : null; }); - - thisNode.append("circle") - .attr("cy", nodeH) - .attr("r", 10) - .attr("class", "WorkflowChart-nodeTypeCircle") - .style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; }); - - thisNode.append("text") - .attr("y", nodeH) - .attr("dy", ".35em") - .attr("text-anchor", "middle") - .attr("class", "WorkflowChart-nodeTypeLetter") - .text(function (d) { - return (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update")) ? "P" : (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? "I" : ""); - }) - .style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; }); - - thisNode.append("rect") - .attr("width", nodeW) - .attr("height", nodeH) - .attr("class", "transparentRect") - .call(edit_node) - .on("mouseover", function(d) { - if(!d.isStartNode) { - d3.select("#node-" + d.id) - .classed("hovering", true); - } - }) - .on("mouseout", function(d){ - if(!d.isStartNode) { - d3.select("#node-" + d.id) - .classed("hovering", false); - } - }); - thisNode.append("text") - .attr("x", nodeW - 50) - .attr("y", nodeH - 10) - .attr("dy", ".35em") - .attr("class", "WorkflowChart-detailsLink") - .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }) - .text(function () { - return "DETAILS"; - }) - .call(details); - thisNode.append("circle") - .attr("id", function(d){return "node-" + d.id + "-add";}) - .attr("cx", nodeW) - .attr("r", 10) - .attr("class", "addCircle nodeCircle") - .style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; }) - .call(add_node) - .on("mouseover", function(d) { - d3.select("#node-" + d.id) - .classed("hovering", true); - d3.select("#node-" + d.id + "-add") - .classed("addHovering", true); - }) - .on("mouseout", function(d){ - d3.select("#node-" + d.id) - .classed("hovering", false); - d3.select("#node-" + d.id + "-add") - .classed("addHovering", false); - }); - thisNode.append("path") - .attr("class", "nodeAddCross WorkflowChart-hoverPath") - .style("fill", "white") - .attr("transform", function() { return "translate(" + nodeW + "," + 0 + ")"; }) - .attr("d", d3.svg.symbol() - .size(60) - .type("cross") - ) - .style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; }) - .call(add_node) - .on("mouseover", function(d) { - d3.select("#node-" + d.id) - .classed("hovering", true); - d3.select("#node-" + d.id + "-add") - .classed("addHovering", true); - }) - .on("mouseout", function(d){ - d3.select("#node-" + d.id) - .classed("hovering", false); - d3.select("#node-" + d.id + "-add") - .classed("addHovering", false); - }); - thisNode.append("circle") - .attr("id", function(d){return "node-" + d.id + "-remove";}) - .attr("cx", nodeW) - .attr("cy", nodeH) - .attr("r", 10) - .attr("class", "removeCircle") - .style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) - .call(remove_node) - .on("mouseover", function(d) { - d3.select("#node-" + d.id) - .classed("hovering", true); - d3.select("#node-" + d.id + "-remove") - .classed("removeHovering", true); - }) - .on("mouseout", function(d){ - d3.select("#node-" + d.id) - .classed("hovering", false); - d3.select("#node-" + d.id + "-remove") - .classed("removeHovering", false); - }); - thisNode.append("path") - .attr("class", "nodeRemoveCross WorkflowChart-hoverPath") - .style("fill", "white") - .attr("transform", function() { return "translate(" + nodeW + "," + nodeH + ") rotate(-45)"; }) - .attr("d", d3.svg.symbol() - .size(60) - .type("cross") - ) - .style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) - .call(remove_node) - .on("mouseover", function(d) { - d3.select("#node-" + d.id) - .classed("hovering", true); - d3.select("#node-" + d.id + "-remove") - .classed("removeHovering", true); - }) - .on("mouseout", function(d){ - d3.select("#node-" + d.id) - .classed("hovering", false); - d3.select("#node-" + d.id + "-remove") - .classed("removeHovering", false); - }); - - thisNode.append("circle") - .attr("class", function(d) { - - let statusClass = "WorkflowChart-nodeStatus "; - - if(d.job){ - switch(d.job.status) { - case "pending": - statusClass += "workflowChart-nodeStatus--running"; - break; - case "waiting": - statusClass += "workflowChart-nodeStatus--running"; - break; - case "running": - statusClass += "workflowChart-nodeStatus--running"; - break; - case "successful": - statusClass += "workflowChart-nodeStatus--success"; - break; - 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.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"; }); - } - }); - - node.exit().remove(); - - let link = svgGroup.selectAll("g.link") - .data(links, function(d) { - return d.source.id + "-" + d.target.id; - }); - - let linkEnter = link.enter().append("g") - .attr("class", "link") - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;}); - - // Add entering links in the parent’s old position. - linkEnter.insert("path", "g") - .attr("class", function(d) { - return (d.source.placeholder || d.target.placeholder) ? "linkPath placeholder" : "linkPath"; - }) - .attr("d", lineData) - .attr('stroke', function(d) { - if(d.target.edgeType) { - if(d.target.edgeType === "failure") { - return "#d9534f"; - } - else if(d.target.edgeType === "success") { - return "#5cb85c"; - } - else if(d.target.edgeType === "always"){ - return "#337ab7"; - } - } - else { - return "#D7D7D7"; - } - }); - - linkEnter.append("circle") - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";}) - .attr("cx", function(d) { - return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2; - }) - .attr("cy", function(d) { - return (d.source.isStartNode) ? ((d.target.x + 10 + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2; - }) - .attr("r", 10) - .attr("class", "addCircle linkCircle") - .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) - .call(add_node_between) - .on("mouseover", function(d) { - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("hovering", true); - d3.select("#link-" + d.source.id + "-" + d.target.id + "-add") - .classed("addHovering", true); - }) - .on("mouseout", function(d){ - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("hovering", false); - d3.select("#link-" + d.source.id + "-" + d.target.id + "-add") - .classed("addHovering", false); - }); - - linkEnter.append("path") - .attr("class", "linkCross") - .style("fill", "white") - .attr("transform", function(d) { - let translate; - if(d.source.isStartNode) { - translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + 10 + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")"; - } - else { - translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")"; - } - return translate; - }) - .attr("d", d3.svg.symbol() - .size(60) - .type("cross") - ) - .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) - .call(add_node_between) - .on("mouseover", function(d) { - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("hovering", true); - d3.select("#link-" + d.source.id + "-" + d.target.id + "-add") - .classed("addHovering", true); - }) - .on("mouseout", function(d){ - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("hovering", false); - d3.select("#link-" + d.source.id + "-" + d.target.id + "-add") - .classed("addHovering", false); - }); - - link.exit().remove(); - - // Transition nodes and links to their new positions. - let t = baseSvg.transition(); - - t.selectAll(".nodeCircle") - .style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; }); - - t.selectAll(".nodeAddCross") - .style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; }); - - t.selectAll(".removeCircle") - .style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }); - - t.selectAll(".nodeRemoveCross") - .style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }); - - t.selectAll(".linkPath") - .attr("class", function(d) { - return (d.source.placeholder || d.target.placeholder) ? "linkPath placeholder" : "linkPath"; + t.selectAll(".linkCircle") + .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) + .attr("cx", function(d) { + return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2; }) - .attr("d", lineData) + .attr("cy", function(d) { + return (d.source.isStartNode) ? ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2; + }); + + t.selectAll(".linkCross") + .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) + .attr("transform", function(d) { + let translate; + if(d.source.isStartNode) { + translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")"; + } + else { + translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")"; + } + return translate; + }); + + t.selectAll(".rect") .attr('stroke', function(d) { - if(d.target.edgeType) { - if(d.target.edgeType === "failure") { - return "#d9534f"; - } - else if(d.target.edgeType === "success") { + if(d.job && d.job.status) { + if(d.job.status === "successful"){ return "#5cb85c"; } - else if(d.target.edgeType === "always"){ - return "#337ab7"; + else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") { + return "#d9534f"; + } + else { + return "#D7D7D7"; } } else { return "#D7D7D7"; } + }) + .attr("class", function(d) { + return d.placeholder ? "rect placeholder" : "rect"; + }); + + 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") + .style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update" ) ? null : "none"; }); + + t.selectAll(".WorkflowChart-nodeTypeLetter") + .text(function (d) { + return (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update")) ? "P" : (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? "I" : ""); + }) + .style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; }); + + t.selectAll(".WorkflowChart-nodeStatus") + .attr("class", function(d) { + + let statusClass = "WorkflowChart-nodeStatus "; + + if(d.job){ + switch(d.job.status) { + case "pending": + statusClass += "workflowChart-nodeStatus--running"; + break; + case "waiting": + statusClass += "workflowChart-nodeStatus--running"; + break; + case "running": + statusClass += "workflowChart-nodeStatus--running"; + break; + case "successful": + statusClass += "workflowChart-nodeStatus--success"; + break; + 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.status ? null : "none"; }) + .transition() + .duration(0) + .attr("r", 6) + .each(function(d) { + 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() { + circle = circle.transition() + .duration(2000) + .attr("r", 6) + .transition() + .duration(2000) + .attr("r", 0) + .ease('sine') + .each("end", repeat); + })(); + } }); - t.selectAll(".linkCircle") - .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) - .attr("cx", function(d) { - return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2; - }) - .attr("cy", function(d) { - return (d.source.isStartNode) ? ((d.target.x + 10 + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2; - }); + t.selectAll(".WorkflowChart-nameText") + .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; }) + .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 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(".linkCross") - .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) - .attr("transform", function(d) { - let translate; - if(d.source.isStartNode) { - translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + 10 + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")"; - } - else { - translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")"; - } - return translate; - }); + t.selectAll(".WorkflowChart-detailsLink") + .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }); - t.selectAll(".rect") - .attr('stroke', function(d) { - if(d.job && d.job.status) { - if(d.job.status === "successful"){ - return "#5cb85c"; - } - else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") { - return "#d9534f"; - } - else { - return "#D7D7D7"; - } - } - else { - return "#D7D7D7"; - } - }) - .attr("class", function(d) { - return d.placeholder ? "rect placeholder" : "rect"; - }); + t.selectAll(".WorkflowChart-incompleteText") + .style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; }); - 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-conflictText") + .style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; }); - t.selectAll(".WorkflowChart-nodeTypeCircle") - .style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update" ) ? null : "none"; }); + t.selectAll(".WorkflowChart-activeNode") + .style("display", function(d) { return d.isActiveEdit ? null : "none"; }); - t.selectAll(".WorkflowChart-nodeTypeLetter") - .text(function (d) { - return (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update")) ? "P" : (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? "I" : ""); - }) - .style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; }); - - t.selectAll(".WorkflowChart-nodeStatus") - .attr("class", function(d) { - - let statusClass = "WorkflowChart-nodeStatus "; - - if(d.job){ - switch(d.job.status) { - case "pending": - statusClass += "workflowChart-nodeStatus--running"; - break; - case "waiting": - statusClass += "workflowChart-nodeStatus--running"; - break; - case "running": - statusClass += "workflowChart-nodeStatus--running"; - break; - case "successful": - statusClass += "workflowChart-nodeStatus--success"; - break; - 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.status ? null : "none"; }) - .transition() - .duration(0) - .attr("r", 6) - .each(function(d) { - 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() { - circle = circle.transition() - .duration(2000) - .attr("r", 6) - .transition() - .duration(2000) - .attr("r", 0) - .ease('sine') - .each("end", repeat); - })(); + t.selectAll(".WorkflowChart-elapsed") + .style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; }); + } + else if(!scope.watchDimensionsSet){ + scope.watchDimensionsSet = scope.$watch('dimensionsSet', function(){ + if(scope.dimensionsSet) { + scope.watchDimensionsSet(); + scope.watchDimensionsSet = null; + update(); } }); - - t.selectAll(".WorkflowChart-nameText") - .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; }) - .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 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.status && d.job.id ? null : "none"; }); - - 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"; }); - - 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() { @@ -809,6 +874,29 @@ export default [ '$state','moment', } }); + function onResize(){ + let dimensions = calcAvailableScreenSpace(); + + $('.WorkflowMaker-chart').css("width", dimensions.width); + $('.WorkflowMaker-chart').css("height", dimensions.height); + } + + function cleanUpResize() { + angular.element($window).off('resize', onResize); + } + + if(scope.mode === 'details') { + angular.element($window).on('resize', onResize); + scope.$on('$destroy', cleanUpResize); + } + else { + scope.$on('workflowMakerModalResized', function(){ + let dimensions = calcAvailableScreenSpace(); + + $('.WorkflowMaker-chart').css("width", dimensions.width); + $('.WorkflowMaker-chart').css("height", dimensions.height); + }); + } } }; }]; 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 e4cbfcd134..7fcf27cc6c 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 @@ -888,6 +888,10 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr }); }; + $scope.$on('WorkflowDialogReady', function(){ + $scope.modalOpen = true; + }); + init(); } diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js index 54c1e526fd..2fc693cda9 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js @@ -6,8 +6,8 @@ import workflowMakerController from './workflow-maker.controller'; -export default ['templateUrl', 'CreateDialog', 'Wait', '$state', - function(templateUrl, CreateDialog, Wait, $state) { +export default ['templateUrl', 'CreateDialog', 'Wait', '$state', '$window', + function(templateUrl, CreateDialog, Wait, $state, $window) { return { scope: { workflowJobTemplateObj: '=', @@ -17,11 +17,17 @@ export default ['templateUrl', 'CreateDialog', 'Wait', '$state', templateUrl: templateUrl('templates/workflows/workflow-maker/workflow-maker'), controller: workflowMakerController, link: function(scope) { + + let availableHeight = $(window).height(), + availableWidth = $(window).width(), + minimumWidth = 1300, + minimumHeight = 550;console.log(availableHeight, availableWidth); + CreateDialog({ id: 'workflow-modal-dialog', scope: scope, - width: 1400, - height: 720, + width: availableWidth > minimumWidth ? availableWidth : minimumWidth, + height: availableHeight > minimumHeight ? availableHeight : minimumHeight, draggable: false, dialogClass: 'SurveyMaker-dialog', position: ['center', 20], @@ -34,6 +40,10 @@ export default ['templateUrl', 'CreateDialog', 'Wait', '$state', // Let the modal height be variable based on the content // and set a uniform padding $('#workflow-modal-dialog').css({ 'padding': '20px' }); + $('#workflow-modal-dialog').parent('.ui-dialog').height(availableHeight > minimumHeight ? availableHeight : minimumHeight); + $('#workflow-modal-dialog').parent('.ui-dialog').width(availableWidth > minimumWidth ? availableWidth : minimumWidth); + $('#workflow-modal-dialog').outerHeight(availableHeight > minimumHeight ? availableHeight : minimumHeight); + $('#workflow-modal-dialog').outerWidth(availableWidth > minimumWidth ? availableWidth : minimumWidth); }, _allowInteraction: function(e) { @@ -55,6 +65,25 @@ export default ['templateUrl', 'CreateDialog', 'Wait', '$state', $state.go('^'); }; + + function onResize(){ + availableHeight = $(window).height(); + availableWidth = $(window).width(); + console.log(availableHeight, availableWidth); + $('#workflow-modal-dialog').parent('.ui-dialog').height(availableHeight > minimumHeight ? availableHeight : minimumHeight); + $('#workflow-modal-dialog').parent('.ui-dialog').width(availableWidth > minimumWidth ? availableWidth : minimumWidth); + $('#workflow-modal-dialog').outerHeight(availableHeight > minimumHeight ? availableHeight : minimumHeight); + $('#workflow-modal-dialog').outerWidth(availableWidth > minimumWidth ? availableWidth : minimumWidth); + + scope.$broadcast('workflowMakerModalResized'); + } + + function cleanUpResize() { + angular.element($window).off('resize', onResize); + } + + angular.element($window).on('resize', onResize); + scope.$on('$destroy', cleanUpResize); } }; } 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 099d03ae10..14f09234aa 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"}}
From 0a02eaa2cf77976c1129e5b98d4f6c4f8f652965 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Fri, 10 Feb 2017 13:31:18 -0500 Subject: [PATCH 052/260] Removed leftover console.log --- .../workflows/workflow-chart/workflow-chart.directive.js | 2 -- .../workflows/workflow-maker/workflow-maker.directive.js | 3 +-- 2 files changed, 1 insertion(+), 4 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 3623de0100..ca5957f134 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 @@ -83,8 +83,6 @@ export default [ '$state','moment', '$timeout', '$window', // This is the workflow editor dimensions.height = $('.WorkflowMaker-contentLeft').outerHeight() - $('.WorkflowLegend-maker').outerHeight(); dimensions.width = $('#workflow-modal-dialog').width() - $('.WorkflowMaker-contentRight').outerWidth(); - - console.log(dimensions); } else { // This is the workflow details view diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js index 2fc693cda9..bfac9c1469 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js @@ -21,7 +21,7 @@ export default ['templateUrl', 'CreateDialog', 'Wait', '$state', '$window', let availableHeight = $(window).height(), availableWidth = $(window).width(), minimumWidth = 1300, - minimumHeight = 550;console.log(availableHeight, availableWidth); + minimumHeight = 550; CreateDialog({ id: 'workflow-modal-dialog', @@ -69,7 +69,6 @@ export default ['templateUrl', 'CreateDialog', 'Wait', '$state', '$window', function onResize(){ availableHeight = $(window).height(); availableWidth = $(window).width(); - console.log(availableHeight, availableWidth); $('#workflow-modal-dialog').parent('.ui-dialog').height(availableHeight > minimumHeight ? availableHeight : minimumHeight); $('#workflow-modal-dialog').parent('.ui-dialog').width(availableWidth > minimumWidth ? availableWidth : minimumWidth); $('#workflow-modal-dialog').outerHeight(availableHeight > minimumHeight ? availableHeight : minimumHeight); From cd27687ef15df1f821240f719ac5343da4dad22e Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Fri, 10 Feb 2017 14:41:36 -0500 Subject: [PATCH 053/260] Re-enabling bootstrap's darkening of disabled fields --- awx/ui/client/legacy-styles/forms.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/legacy-styles/forms.less b/awx/ui/client/legacy-styles/forms.less index 570a096c7e..5da836f921 100644 --- a/awx/ui/client/legacy-styles/forms.less +++ b/awx/ui/client/legacy-styles/forms.less @@ -245,13 +245,13 @@ .Form-textArea{ border-radius: 5px; color: @field-input-text; - background-color: @field-secondary-bg!important; + background-color: @field-secondary-bg; width:100%!important; } .Form-textInput{ height: 30px; - background-color: @field-secondary-bg!important; + background-color: @field-secondary-bg; border-radius: 5px; border:1px solid @field-border; color: @field-input-text; From 84b426079e37addee9310c1ce3f8266b16c86211 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Fri, 10 Feb 2017 15:47:14 -0500 Subject: [PATCH 054/260] Restore the ability to specify a search field for column sorting --- awx/ui/client/src/lists/ScheduledJobs.js | 3 ++- .../client/src/shared/list-generator/list-generator.factory.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/lists/ScheduledJobs.js b/awx/ui/client/src/lists/ScheduledJobs.js index 736ad61e4e..8c016cfb9f 100644 --- a/awx/ui/client/src/lists/ScheduledJobs.js +++ b/awx/ui/client/src/lists/ScheduledJobs.js @@ -46,7 +46,8 @@ export default columnClass: "col-lg-2 col-md-2 hidden-sm hidden-xs", sourceModel: 'unified_job_template', sourceField: 'unified_job_type', - ngBind: 'schedule.type_label' + ngBind: 'schedule.type_label', + searchField: 'unified_job_template__polymorphic_ctype__model' }, next_run: { label: i18n._('Next Run'), 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 0140169f79..3e425a0828 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 @@ -494,7 +494,7 @@ export default ['$location', '$compile', '$rootScope', 'Attr', 'Icon', collection="${list.name}" dataset="${list.iterator}_dataset" column-sort - column-field="${fld}" + column-field="${list.fields[fld].searchField || fld}" column-iterator="${list.iterator}" column-no-sort="${list.fields[fld].nosort}" column-label="${list.fields[fld].label}" From d912013db759edc4ec56006fcec3790c811f9e66 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 10 Feb 2017 15:57:24 -0500 Subject: [PATCH 055/260] fix some bugs with the logging reload approach --- awx/main/utils/handlers.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index 08005f7ee4..d115df11c7 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -174,7 +174,7 @@ class HTTPSHandler(object): return BaseHTTPSHandler.from_django_settings(settings_module, *args, **kwargs) -def add_or_remove_logger(address, instance, adding=True): +def add_or_remove_logger(address, instance): specific_logger = logging.getLogger(address) i_occurance = None for i in range(len(specific_logger.handlers)): @@ -182,12 +182,14 @@ def add_or_remove_logger(address, instance, adding=True): i_occurance = i break - if i_occurance is None and not adding: - return - elif i_occurance is None: - specific_logger.handlers.append(instance) + if i_occurance is None: + if instance is not None: + specific_logger.handlers.append(instance) else: - specific_logger.handlers[i_occurance] = instance + if instance is None: + specific_logger.handlers[i_occurance] = HTTPSNullHandler() + else: + specific_logger.handlers[i_occurance] = instance def configure_external_logger(settings_module, async_flag=True, is_startup=True): @@ -197,11 +199,13 @@ def configure_external_logger(settings_module, async_flag=True, is_startup=True) # Pass-through if external logging not being used return + instance = None if is_enabled: instance = HTTPSHandler(settings_module, async=async_flag) instance.setFormatter(LogstashFormatter()) - else: - instance = HTTPSNullHandler() + awx_logger_instance = instance + if is_enabled and 'awx' not in settings_module.LOG_AGGREGATOR_LOGGERS: + awx_logger_instance = None - add_or_remove_logger('awx.analytics', instance, adding=is_enabled) - add_or_remove_logger('awx', instance, adding=(is_enabled and 'awx' in settings_module.LOG_AGGREGATOR_LOGGERS)) + add_or_remove_logger('awx.analytics', instance) + add_or_remove_logger('awx', awx_logger_instance) From 5f10f4c51c1ffb5948901de81c2b01016dea8cbc Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Fri, 10 Feb 2017 15:04:16 -0800 Subject: [PATCH 056/260] adding null object fix for ProcessErrors module --- awx/ui/client/src/shared/Utilities.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index 3bb9c02de0..81598e00f0 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -192,7 +192,7 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter']) } if (status === 403) { msg = 'The API responded with a 403 Access Denied error. '; - if (data.detail) { + if (data && data.detail) { msg += 'Detail: ' + data.detail; } else { msg += 'Please contact your system administrator.'; From 7c16dac189574a554b2550cb3c86d0dde6f23a53 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Fri, 10 Feb 2017 17:07:56 -0800 Subject: [PATCH 057/260] changing URL to 'groups' instead of 'hosts' for CopyMoveHosts --- .../src/inventories/manage/copy-move/copy-move.route.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 c80d9b0591..deff0cf0ef 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 @@ -67,7 +67,7 @@ var copyMoveHostRoute = { resolve: { Dataset: ['CopyMoveGroupList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { - let path = GetBasePath('inventory') + $stateParams.inventory_id + '/hosts/'; + let path = GetBasePath('inventory') + $stateParams.inventory_id + '/groups/'; return qs.search(path, $stateParams.copy_search); } ], @@ -83,7 +83,7 @@ var copyMoveHostRoute = { 'copyMoveList@inventoryManage.copyMoveHost': { templateProvider: function(CopyMoveGroupList, generateList, $stateParams, GetBasePath) { let list = CopyMoveGroupList; - list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/hosts/'; + list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/groups/'; let html = generateList.build({ list: CopyMoveGroupList, mode: 'lookup', From 40c50300f74a7058173ff58ebb0cf407c4f3c387 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Fri, 10 Feb 2017 20:09:39 -0500 Subject: [PATCH 058/260] Fix bug with job_type = scan not loading Default project correctly --- .../edit-job-template/job-template-edit.controller.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js index 8da8a4c034..c6e2770a50 100644 --- a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js +++ b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js @@ -53,6 +53,10 @@ export default $scope.parseType = 'yaml'; $scope.showJobType = false; + if(!$scope.project) { + $scope.project_name = 'Default'; + } + SurveyControllerInit({ scope: $scope, parent_scope: $scope, From 1df4adc957e7c88bbf4b74649b1de920c1556ff2 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Fri, 10 Feb 2017 20:16:34 -0500 Subject: [PATCH 059/260] Check job_type as well as !project --- .../edit-job-template/job-template-edit.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js index c6e2770a50..8312c09543 100644 --- a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js +++ b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js @@ -53,7 +53,7 @@ export default $scope.parseType = 'yaml'; $scope.showJobType = false; - if(!$scope.project) { + if($scope.job_type && $scope.job_type.value === 'scan' && !$scope.project) { $scope.project_name = 'Default'; } From c59d82bd055f59f06ab69cfc7a3e9084f387b57a Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Fri, 10 Feb 2017 16:07:48 -0500 Subject: [PATCH 060/260] Update name/description whenever the activities array changes --- awx/ui/client/src/widgets/Stream.js | 31 +++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/awx/ui/client/src/widgets/Stream.js b/awx/ui/client/src/widgets/Stream.js index a41b14b462..1fc1a40520 100644 --- a/awx/ui/client/src/widgets/Stream.js +++ b/awx/ui/client/src/widgets/Stream.js @@ -283,18 +283,29 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti }); }; - scope.activities.forEach(function(activity, i) { - // build activity.user - if (scope.activities[i].summary_fields.actor) { - scope.activities[i].user = "" + - scope.activities[i].summary_fields.actor.username + ""; - } else { - scope.activities[i].user = 'system'; - } - // build description column / action text - BuildDescription(scope.activities[i]); + if(scope.activities && scope.activities.length > 0) { + buildUserAndDescription(); + } + scope.$watch('activities', function(){ + // Watch for future update to scope.activities (like page change, column sort, search, etc) + buildUserAndDescription(); }); + + function buildUserAndDescription(){ + scope.activities.forEach(function(activity, i) { + // build activity.user + if (scope.activities[i].summary_fields.actor) { + scope.activities[i].user = "" + + scope.activities[i].summary_fields.actor.username + ""; + } else { + scope.activities[i].user = 'system'; + } + // build description column / action text + BuildDescription(scope.activities[i]); + + }); + } }; } ]); From f02b5415afe6b1ab6a66f2af14de80a2195703a7 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Fri, 10 Feb 2017 23:45:19 -0500 Subject: [PATCH 061/260] add auth token to logout request header --- .../login/authenticationServices/authentication.service.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/login/authenticationServices/authentication.service.js b/awx/ui/client/src/login/authenticationServices/authentication.service.js index b6064e8211..9771bc46c5 100644 --- a/awx/ui/client/src/login/authenticationServices/authentication.service.js +++ b/awx/ui/client/src/login/authenticationServices/authentication.service.js @@ -61,7 +61,10 @@ export default deleteToken: function () { return $http({ method: 'DELETE', - url: GetBasePath('authtoken') + url: GetBasePath('authtoken'), + headers: { + 'Authorization': 'Token ' + this.getToken() + } }); }, From 263aa866a36e55630ad692e006e9e23f0148ae99 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Mon, 13 Feb 2017 08:15:13 -0500 Subject: [PATCH 062/260] Removed help tooltip for disabled in file fields --- .../configuration/auth-form/configuration-auth.controller.js | 3 ++- .../configuration/jobs-form/configuration-jobs.controller.js | 3 ++- .../system-form/configuration-system.controller.js | 3 ++- .../src/configuration/ui-form/configuration-ui.controller.js | 3 ++- 4 files changed, 8 insertions(+), 4 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 07c5cc8a0b..a674eb76aa 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 @@ -163,7 +163,8 @@ export default [ function addFieldInfo(form, key) { _.extend(form.fields[key], { - awPopOver: $scope.$parent.configDataResolve[key].help_text, + awPopOver: ($scope.$parent.configDataResolve[key].defined_in_file) ? + null: $scope.$parent.configDataResolve[key].help_text, label: $scope.$parent.configDataResolve[key].label, name: key, toggleSource: key, 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 af0572481b..281edcbff8 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 @@ -49,7 +49,8 @@ export default [ function addFieldInfo(form, key) { _.extend(form.fields[key], { - awPopOver: $scope.$parent.configDataResolve[key].help_text, + awPopOver: ($scope.$parent.configDataResolve[key].defined_in_file) ? + null: $scope.$parent.configDataResolve[key].help_text, label: $scope.$parent.configDataResolve[key].label, name: key, toggleSource: 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 865145115b..3a3a1b789d 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 @@ -122,7 +122,8 @@ export default [ function addFieldInfo(form, key) { _.extend(form.fields[key], { - awPopOver: $scope.$parent.configDataResolve[key].help_text, + awPopOver: ($scope.$parent.configDataResolve[key].defined_in_file) ? + null: $scope.$parent.configDataResolve[key].help_text, label: $scope.$parent.configDataResolve[key].label, name: key, toggleSource: 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 43ad4df2dc..77fa9da011 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 @@ -52,7 +52,8 @@ function addFieldInfo(form, key) { _.extend(form.fields[key], { - awPopOver: $scope.$parent.configDataResolve[key].help_text, + awPopOver: ($scope.$parent.configDataResolve[key].defined_in_file) ? + null: $scope.$parent.configDataResolve[key].help_text, label: $scope.$parent.configDataResolve[key].label, name: key, toggleSource: key, From 2cbf61bc32caeba293154f56d8ffe1337af242f5 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Mon, 13 Feb 2017 10:58:51 -0500 Subject: [PATCH 063/260] Updated inventory search bar width and buttons --- awx/ui/client/legacy-styles/lists.less | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index 4aa4f560c9..37e34e2515 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -432,3 +432,28 @@ table, tbody { margin-left: 0; } } + +.InventoryManage-container { + .List-header { + flex-direction: column; + align-items: stretch; + } + .List-actionHolder { + justify-content: flex-start; + align-items: center; + flex: 1 0 auto; + margin-top: 12px; + } + .List-actions { + margin-bottom: 20px; + } + .List-well { + margin-top: 20px; + } + .List-action:not(.ng-hide) ~ .List-action:not(.ng-hide) { + margin-left: 0; + } +} + + + From 8c4d4a0543f13d172f8385a3f8499068a478f31e Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 13 Feb 2017 11:54:41 -0500 Subject: [PATCH 064/260] Protect Tower from Ansible core making changes to the event uuids --- awx/main/management/commands/run_callback_receiver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/main/management/commands/run_callback_receiver.py b/awx/main/management/commands/run_callback_receiver.py index e984e41bf4..c262b23024 100644 --- a/awx/main/management/commands/run_callback_receiver.py +++ b/awx/main/management/commands/run_callback_receiver.py @@ -70,8 +70,11 @@ class CallbackBrokerWorker(ConsumerMixin): callbacks=[self.process_task])] def process_task(self, body, message): - if "uuid" in body: - queue = UUID(body['uuid']).int % settings.JOB_EVENT_WORKERS + if "uuid" in body and body['uuid']: + try: + queue = UUID(body['uuid']).int % settings.JOB_EVENT_WORKERS + except Exception: + queue = self.total_messages % settings.JOB_EVENT_WORKERS else: queue = self.total_messages % settings.JOB_EVENT_WORKERS self.write_queue_worker(queue, body) From b0e992d6abacda5339a198ecc4344c076a847789 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 13 Feb 2017 12:34:21 -0500 Subject: [PATCH 065/260] disable computed field signals in cleanup_jobs --- awx/main/management/commands/cleanup_jobs.py | 26 +++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/awx/main/management/commands/cleanup_jobs.py b/awx/main/management/commands/cleanup_jobs.py index 3b26767bc0..84927c755a 100644 --- a/awx/main/management/commands/cleanup_jobs.py +++ b/awx/main/management/commands/cleanup_jobs.py @@ -12,7 +12,15 @@ from django.db import transaction from django.utils.timezone import now # AWX -from awx.main.models import Job, AdHocCommand, ProjectUpdate, InventoryUpdate, SystemJob, WorkflowJob, Notification +from awx.main.models import ( + Job, AdHocCommand, ProjectUpdate, InventorySource, InventoryUpdate, + SystemJob, WorkflowJob, Notification, Group, Host +) +from awx.main.signals import ( # noqa + emit_update_inventory_on_created_or_deleted, + emit_update_inventory_computed_fields +) +from django.db.models.signals import post_save, post_delete, m2m_changed # noqa class Command(NoArgsCommand): @@ -219,12 +227,28 @@ class Command(NoArgsCommand): deleted += 1 return skipped, deleted + def disable_job_signals(self): + sigstat = [] + sigstat.append(post_save.disconnect(emit_update_inventory_on_created_or_deleted, sender=Host)) + sigstat.append(post_delete.disconnect(emit_update_inventory_on_created_or_deleted, sender=Host)) + sigstat.append(post_save.disconnect(emit_update_inventory_on_created_or_deleted, sender=Group)) + sigstat.append(post_delete.disconnect(emit_update_inventory_on_created_or_deleted, sender=Group)) + sigstat.append(m2m_changed.disconnect(emit_update_inventory_computed_fields, sender=Group.hosts.through)) + sigstat.append(m2m_changed.disconnect(emit_update_inventory_computed_fields, sender=Group.parents.through)) + sigstat.append(m2m_changed.disconnect(emit_update_inventory_computed_fields, sender=Host.inventory_sources.through)) + sigstat.append(m2m_changed.disconnect(emit_update_inventory_computed_fields, sender=Group.inventory_sources.through)) + sigstat.append(post_save.disconnect(emit_update_inventory_on_created_or_deleted, sender=InventorySource)) + sigstat.append(post_delete.disconnect(emit_update_inventory_on_created_or_deleted, sender=InventorySource)) + sigstat.append(post_save.disconnect(emit_update_inventory_on_created_or_deleted, sender=Job)) + sigstat.append(post_delete.disconnect(emit_update_inventory_on_created_or_deleted, sender=Job)) + @transaction.atomic def handle_noargs(self, **options): self.verbosity = int(options.get('verbosity', 1)) self.init_logging() self.days = int(options.get('days', 90)) self.dry_run = bool(options.get('dry_run', False)) + self.disable_job_signals() try: self.cutoff = now() - datetime.timedelta(days=self.days) except OverflowError: From ee25be1e67916aa94ddf5776680d34edfe2e9ad6 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 13 Feb 2017 12:34:47 -0500 Subject: [PATCH 066/260] Allow execute role to see their schedules --- awx/main/access.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index c18b4cfd6f..e45eb932bc 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1952,13 +1952,9 @@ class ScheduleAccess(BaseAccess): qs = qs.prefetch_related('unified_job_template') if self.user.is_superuser or self.user.is_system_auditor: return qs.all() - job_template_qs = self.user.get_queryset(JobTemplate) - inventory_source_qs = self.user.get_queryset(InventorySource) - project_qs = self.user.get_queryset(Project) - unified_qs = UnifiedJobTemplate.objects.filter(jobtemplate__in=job_template_qs) | \ - UnifiedJobTemplate.objects.filter(Q(project__in=project_qs)) | \ - UnifiedJobTemplate.objects.filter(Q(inventorysource__in=inventory_source_qs)) - return qs.filter(unified_job_template__in=unified_qs) + + unified_qs = UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role') + return qs.filter(unified_job_template__id__in=unified_qs) @check_superuser def can_read(self, obj): From 1ebb641c1e0cd08bc2c3e55543974c363163cb30 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Mon, 13 Feb 2017 12:43:13 -0500 Subject: [PATCH 067/260] work around a DRF issue that causes CharField to cast `None` to `"None"` see: #5322 --- awx/conf/fields.py | 12 ++++++++++++ awx/conf/tests/unit/test_settings.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/awx/conf/fields.py b/awx/conf/fields.py index 13d80ae937..f8d012a3aa 100644 --- a/awx/conf/fields.py +++ b/awx/conf/fields.py @@ -19,6 +19,18 @@ logger = logging.getLogger('awx.conf.fields') # appropriate Python type to be used in settings. +class CharField(CharField): + + def to_representation(self, value): + # django_rest_frameworks' default CharField implementation casts `None` + # to a string `"None"`: + # + # https://github.com/tomchristie/django-rest-framework/blob/cbad236f6d817d992873cd4df6527d46ab243ed1/rest_framework/fields.py#L761 + if value is None: + return None + return super(CharField, self).to_representation(value) + + class StringListField(ListField): child = CharField() diff --git a/awx/conf/tests/unit/test_settings.py b/awx/conf/tests/unit/test_settings.py index 5cd5d0e012..3fc78c452d 100644 --- a/awx/conf/tests/unit/test_settings.py +++ b/awx/conf/tests/unit/test_settings.py @@ -9,10 +9,9 @@ from django.conf import LazySettings from django.core.cache.backends.locmem import LocMemCache from django.core.exceptions import ImproperlyConfigured from django.utils.translation import ugettext_lazy as _ -from rest_framework import fields import pytest -from awx.conf import models +from awx.conf import models, fields from awx.conf.settings import SettingsWrapper, EncryptedCacheProxy, SETTING_CACHE_NOTSET from awx.conf.registry import SettingsRegistry @@ -330,6 +329,31 @@ def test_read_only_setting_deletion(settings): assert settings.AWX_SOME_SETTING == 'DEFAULT' +def test_charfield_properly_sets_none(settings, mocker): + "see: https://github.com/ansible/ansible-tower/issues/5322" + settings.registry.register( + 'AWX_SOME_SETTING', + field_class=fields.CharField, + category=_('System'), + category_slug='system', + allow_null=True + ) + + setting_list = mocker.Mock(**{'order_by.return_value.first.return_value': None}) + with apply_patches([ + mocker.patch('awx.conf.models.Setting.objects.filter', + return_value=setting_list), + mocker.patch('awx.conf.models.Setting.objects.create', mocker.Mock()) + ]): + settings.AWX_SOME_SETTING = None + + models.Setting.objects.create.assert_called_with( + key='AWX_SOME_SETTING', + user=None, + value=None + ) + + def test_settings_use_an_encrypted_cache(settings): settings.registry.register( 'AWX_ENCRYPTED', From b693b06706172d06f14dc9835b2dcfa7d48f56ea Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 13 Feb 2017 12:38:42 -0500 Subject: [PATCH 068/260] remove partial dependency job id logic * Late in the release we added job dependency tracking to the DB. We decided not to use this information in the scheduler. However, I half-ass added code to the scheduler to use it. Note that we still determine inv and job update dependency by using a hack of the related creation time, job.created-2 and job.created-1 respectively. This removes any use of job dependent id expect for purposes of chain failing. --- awx/main/scheduler/partial.py | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/awx/main/scheduler/partial.py b/awx/main/scheduler/partial.py index 6cb87add15..6cba9c35b8 100644 --- a/awx/main/scheduler/partial.py +++ b/awx/main/scheduler/partial.py @@ -1,7 +1,6 @@ # Python import json -import itertools # AWX from awx.main.utils import decrypt_field_value @@ -62,35 +61,13 @@ 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: - 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', 'dependent_jobs__id', + 'forks', 'start_args', ) model = Job @@ -113,8 +90,7 @@ class JobDict(PartialModelDict): 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] + return [cls(o) for o in cls.model.objects.filter(**kv).values(*cls.get_db_values())] class ProjectUpdateDict(PartialModelDict): From 4e6773b9229c2cbb72d3793dc8fe77a71814b9c7 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Mon, 13 Feb 2017 13:49:54 -0500 Subject: [PATCH 069/260] Changed inputs to textareas, support key and cert --- .../configuration/auth-form/sub-forms/auth-saml.form.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 ad1f7cb6d8..1a972f0aee 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 @@ -24,11 +24,15 @@ export default ['i18n', function(i18n) { reset: 'SOCIAL_AUTH_SAML_SP_ENTITY_ID' }, SOCIAL_AUTH_SAML_SP_PUBLIC_CERT: { - type: 'text', + type: 'textarea', + rows: 6, + elementClass: 'Form-monospace', reset: 'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT' }, SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: { - type: 'sensitive', + type: 'textarea', + rows: 6, + elementClass: 'Form-monospace', hasShowInputButton: true, reset: 'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY' }, From 31098d1b5ded77c60c01a04221506d64590edaad Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 13 Feb 2017 14:34:40 -0500 Subject: [PATCH 070/260] surface labels related search for job templates --- awx/api/generics.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 5e81ee7bdb..001b4b0305 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -285,7 +285,11 @@ class ListAPIView(generics.ListAPIView, GenericAPIView): if name.endswith('_set'): continue fields.append('{}__search'.format(name)) - for relationship in self.model._meta.local_many_to_many: + m2m_rel = [] + m2m_rel += self.model._meta.local_many_to_many + if issubclass(self.model, UnifiedJobTemplate): + m2m_rel += UnifiedJobTemplate._meta.local_many_to_many + for relationship in m2m_rel: if relationship.related_model._meta.app_label != 'main': continue fields.append('{}__search'.format(relationship.name)) From 603dfea58082e22ef01ec23d1c2bbf1c271aded7 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 13 Feb 2017 14:54:02 -0500 Subject: [PATCH 071/260] get rid of HTTPSHandler subclass which is not needed --- awx/main/utils/handlers.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index d115df11c7..cc27823751 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -16,7 +16,7 @@ from requests_futures.sessions import FuturesSession from awx.main.utils.formatters import LogstashFormatter -__all__ = ['HTTPSNullHandler', 'BaseHTTPSHandler', 'HTTPSHandler', 'configure_external_logger'] +__all__ = ['HTTPSNullHandler', 'BaseHTTPSHandler', 'configure_external_logger'] # AWX external logging handler, generally designed to be used # with the accompanying LogstashHandler, derives from python-logstash library @@ -168,16 +168,10 @@ class BaseHTTPSHandler(logging.Handler): **self.get_post_kwargs(payload)) -class HTTPSHandler(object): - - def __new__(cls, settings_module, *args, **kwargs): - return BaseHTTPSHandler.from_django_settings(settings_module, *args, **kwargs) - - def add_or_remove_logger(address, instance): specific_logger = logging.getLogger(address) i_occurance = None - for i in range(len(specific_logger.handlers)): + for i, _ in enumerate(specific_logger.handlers): if isinstance(specific_logger.handlers[i], (HTTPSNullHandler, BaseHTTPSHandler)): i_occurance = i break @@ -201,7 +195,7 @@ def configure_external_logger(settings_module, async_flag=True, is_startup=True) instance = None if is_enabled: - instance = HTTPSHandler(settings_module, async=async_flag) + instance = BaseHTTPSHandler.from_django_settings(settings_module, async=async_flag) instance.setFormatter(LogstashFormatter()) awx_logger_instance = instance if is_enabled and 'awx' not in settings_module.LOG_AGGREGATOR_LOGGERS: From 4035910e3e7e3fa79756fb42570a85c615524904 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 13 Feb 2017 15:20:14 -0500 Subject: [PATCH 072/260] changing to ng-if so that directive doesn't fire off --- .../src/access/add-rbac-resource/rbac-resource.partial.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html index 3047469ca7..b664f81378 100644 --- a/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html +++ b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html @@ -47,7 +47,7 @@
-
+
From 6b66a61dada4c3634c7221dd51c0ce11835e3c1c Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 13 Feb 2017 15:32:48 -0500 Subject: [PATCH 073/260] Resize the workflow chart when expanded out to full screen --- .../workflows/workflow-chart/workflow-chart.directive.js | 8 ++++++++ .../src/workflow-results/workflow-results.controller.js | 3 +++ 2 files changed, 11 insertions(+) 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 ca5957f134..ea2fa33884 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 @@ -886,6 +886,14 @@ export default [ '$state','moment', '$timeout', '$window', if(scope.mode === 'details') { angular.element($window).on('resize', onResize); scope.$on('$destroy', cleanUpResize); + + scope.$on('workflowDetailsResized', function(){ + $('.WorkflowMaker-chart').hide(); + $timeout(function(){ + onResize(); + $('.WorkflowMaker-chart').show(); + }); + }); } else { scope.$on('workflowMakerModalResized', function(){ 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 93bfbbf718..cd4ae42a20 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.controller.js +++ b/awx/ui/client/src/workflow-results/workflow-results.controller.js @@ -130,6 +130,9 @@ export default ['workflowData', } $scope.toggleStdoutFullscreen = function() { + + $scope.$broadcast('workflowDetailsResized'); + $scope.stdoutFullScreen = !$scope.stdoutFullScreen; if ($scope.stdoutFullScreen === true) { From fbe712dcd14dcda9099993e71c9be6537830d7f1 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 13 Feb 2017 15:45:05 -0500 Subject: [PATCH 074/260] refactor adding logger loop from PR review --- awx/main/utils/handlers.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index cc27823751..f3e555c4b5 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -170,20 +170,13 @@ class BaseHTTPSHandler(logging.Handler): def add_or_remove_logger(address, instance): specific_logger = logging.getLogger(address) - i_occurance = None - for i, _ in enumerate(specific_logger.handlers): - if isinstance(specific_logger.handlers[i], (HTTPSNullHandler, BaseHTTPSHandler)): - i_occurance = i + for i, handler in enumerate(specific_logger.handlers): + if isinstance(handler, (HTTPSNullHandler, BaseHTTPSHandler)): + specific_logger.handlers[i] = instance or HTTPSNullHandler() break - - if i_occurance is None: + else: if instance is not None: specific_logger.handlers.append(instance) - else: - if instance is None: - specific_logger.handlers[i_occurance] = HTTPSNullHandler() - else: - specific_logger.handlers[i_occurance] = instance def configure_external_logger(settings_module, async_flag=True, is_startup=True): From 2c7cb4a370bb847b43b98fd09ef358cd73af6daa Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 10 Feb 2017 14:30:42 -0500 Subject: [PATCH 075/260] add utf-8 support to utils.common.encrypt_field/decrypt_field --- .../tests/unit/utils/common/test_common.py | 19 ++++++++++++--- awx/main/utils/common.py | 23 ++++++++++++++++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/awx/main/tests/unit/utils/common/test_common.py b/awx/main/tests/unit/utils/common/test_common.py index a152d69c12..a48bbe64b3 100644 --- a/awx/main/tests/unit/utils/common/test_common.py +++ b/awx/main/tests/unit/utils/common/test_common.py @@ -1,24 +1,37 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 Ansible, Inc. +# All Rights Reserved. + from awx.conf.models import Setting from awx.main.utils import common def test_encrypt_field(): field = Setting(pk=123, value='ANSIBLE') - encrypted = common.encrypt_field(field, 'value') + encrypted = field.value = common.encrypt_field(field, 'value') assert encrypted == '$encrypted$AES$Ey83gcmMuBBT1OEq2lepnw==' assert common.decrypt_field(field, 'value') == 'ANSIBLE' def test_encrypt_field_without_pk(): field = Setting(value='ANSIBLE') - encrypted = common.encrypt_field(field, 'value') + encrypted = field.value = common.encrypt_field(field, 'value') assert encrypted == '$encrypted$AES$8uIzEoGyY6QJwoTWbMFGhw==' assert common.decrypt_field(field, 'value') == 'ANSIBLE' +def test_encrypt_field_with_unicode_string(): + value = u'Iñtërnâtiônàlizætiøn' + field = Setting(value=value) + encrypted = field.value = common.encrypt_field(field, 'value') + assert encrypted == '$encrypted$UTF8$AES$AESQbqOefpYcLC7x8yZ2aWG4FlXlS66JgavLbDp/DSM=' + assert common.decrypt_field(field, 'value') == value + + def test_encrypt_subfield(): field = Setting(value={'name': 'ANSIBLE'}) - encrypted = common.encrypt_field(field, 'value', subfield='name') + encrypted = field.value = common.encrypt_field(field, 'value', subfield='name') assert encrypted == '$encrypted$AES$8uIzEoGyY6QJwoTWbMFGhw==' assert common.decrypt_field(field, 'value', subfield='name') == 'ANSIBLE' diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 5842b78db9..e49f4d0131 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -21,6 +21,8 @@ import tempfile # Decorator from decorator import decorator +import six + # Django from django.utils.translation import ugettext_lazy as _ from django.db.models import ManyToManyField @@ -190,6 +192,7 @@ def encrypt_field(instance, field_name, ask=False, subfield=None): value = value[subfield] if not value or value.startswith('$encrypted$') or (ask and value == 'ASK'): return value + utf8 = type(value) == six.text_type value = smart_str(value) key = get_encryption_key(field_name, getattr(instance, 'pk', None)) cipher = AES.new(key, AES.MODE_ECB) @@ -197,17 +200,31 @@ def encrypt_field(instance, field_name, ask=False, subfield=None): value += '\x00' encrypted = cipher.encrypt(value) b64data = base64.b64encode(encrypted) - return '$encrypted$%s$%s' % ('AES', b64data) + tokens = ['$encrypted', 'AES', b64data] + if utf8: + # If the value to encrypt is utf-8, we need to add a marker so we + # know to decode the data when it's decrypted later + tokens.insert(1, 'UTF8') + return '$'.join(tokens) def decrypt_value(encryption_key, value): - algo, b64data = value[len('$encrypted$'):].split('$', 1) + raw_data = value[len('$encrypted$'):] + # If the encrypted string contains a UTF8 marker, discard it + utf8 = raw_data.startswith('UTF8$') + if utf8: + raw_data = raw_data[len('UTF8$'):] + algo, b64data = raw_data.split('$', 1) if algo != 'AES': raise ValueError('unsupported algorithm: %s' % algo) encrypted = base64.b64decode(b64data) cipher = AES.new(encryption_key, AES.MODE_ECB) value = cipher.decrypt(encrypted) - return value.rstrip('\x00') + value = value.rstrip('\x00') + # If the encrypted string contained a UTF8 marker, decode the data + if utf8: + value = value.decode('utf-8') + return value def decrypt_field(instance, field_name, subfield=None): From 64a973ae0249f7995c6800d932499c4710eb0295 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 10 Feb 2017 10:41:28 -0500 Subject: [PATCH 076/260] work around a unicode handling bug in python-memcached that affects py2 see: https://github.com/linsomniac/python-memcached/issues/79 see: #5276 --- awx/conf/settings.py | 14 ++++++++++- awx/conf/tests/unit/test_settings.py | 37 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/awx/conf/settings.py b/awx/conf/settings.py index 8b1c0786a1..43ad9ee61f 100644 --- a/awx/conf/settings.py +++ b/awx/conf/settings.py @@ -6,6 +6,8 @@ import sys import threading import time +import six + # Django from django.conf import settings, UserSettingsHolder from django.core.cache import cache as django_cache @@ -88,7 +90,17 @@ class EncryptedCacheProxy(object): def get(self, key, **kwargs): value = self.cache.get(key, **kwargs) - return self._handle_encryption(self.decrypter, key, value) + value = self._handle_encryption(self.decrypter, key, value) + + # python-memcached auto-encodes unicode on cache set in python2 + # https://github.com/linsomniac/python-memcached/issues/79 + # https://github.com/linsomniac/python-memcached/blob/288c159720eebcdf667727a859ef341f1e908308/memcache.py#L961 + if six.PY2 and isinstance(value, six.binary_type): + try: + six.text_type(value) + except UnicodeDecodeError: + value = value.decode('utf-8') + return value def set(self, key, value, **kwargs): self.cache.set( diff --git a/awx/conf/tests/unit/test_settings.py b/awx/conf/tests/unit/test_settings.py index 3fc78c452d..f7f1540108 100644 --- a/awx/conf/tests/unit/test_settings.py +++ b/awx/conf/tests/unit/test_settings.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + # Copyright (c) 2017 Ansible, Inc. # All Rights Reserved. @@ -10,6 +12,7 @@ from django.core.cache.backends.locmem import LocMemCache from django.core.exceptions import ImproperlyConfigured from django.utils.translation import ugettext_lazy as _ import pytest +import six from awx.conf import models, fields from awx.conf.settings import SettingsWrapper, EncryptedCacheProxy, SETTING_CACHE_NOTSET @@ -60,6 +63,15 @@ def test_unregistered_setting(settings): assert settings.cache.get('DEBUG') is None +def test_cached_settings_unicode_is_auto_decoded(settings): + # https://github.com/linsomniac/python-memcached/issues/79 + # https://github.com/linsomniac/python-memcached/blob/288c159720eebcdf667727a859ef341f1e908308/memcache.py#L961 + + value = six.u('Iñtërnâtiônàlizætiøn').encode('utf-8') # this simulates what python-memcached does on cache.set() + settings.cache.set('DEBUG', value) + assert settings.cache.get('DEBUG') == six.u('Iñtërnâtiônàlizætiøn') + + def test_read_only_setting(settings): settings.registry.register( 'AWX_READ_ONLY', @@ -239,6 +251,31 @@ def test_setting_from_db(settings, mocker): assert settings.cache.get('AWX_SOME_SETTING') == 'FROM_DB' +@pytest.mark.parametrize('encrypted', (True, False)) +def test_setting_from_db_with_unicode(settings, mocker, encrypted): + settings.registry.register( + 'AWX_SOME_SETTING', + field_class=fields.CharField, + category=_('System'), + category_slug='system', + default='DEFAULT', + encrypted=encrypted + ) + # this simulates a bug in python-memcached; see https://github.com/linsomniac/python-memcached/issues/79 + value = six.u('Iñtërnâtiônàlizætiøn').encode('utf-8') + + setting_from_db = mocker.Mock(key='AWX_SOME_SETTING', value=value) + mocks = mocker.Mock(**{ + 'order_by.return_value': mocker.Mock(**{ + '__iter__': lambda self: iter([setting_from_db]), + 'first.return_value': setting_from_db + }), + }) + with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=mocks): + assert settings.AWX_SOME_SETTING == six.u('Iñtërnâtiônàlizætiøn') + assert settings.cache.get('AWX_SOME_SETTING') == six.u('Iñtërnâtiônàlizætiøn') + + @pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT') def test_read_only_setting_assignment(settings): "read-only settings cannot be overwritten" From 5a8a647cf0376ec09565c1d411329900b8645e2c Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Mon, 13 Feb 2017 16:07:07 -0500 Subject: [PATCH 077/260] default log aggregator username and password to an empty string other configuration options seem to follow this pattern; the UI code seems to expect that it can send across an empty string see: #5276 --- awx/main/conf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index d82e766607..6bb4c15895 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -258,7 +258,8 @@ register( register( 'LOG_AGGREGATOR_USERNAME', field_class=fields.CharField, - allow_null=True, + allow_blank=True, + default='', label=_('Logging Aggregator Username'), help_text=_('Username for external log aggregator (if required).'), category=_('Logging'), @@ -268,7 +269,8 @@ register( register( 'LOG_AGGREGATOR_PASSWORD', field_class=fields.CharField, - allow_null=True, + allow_blank=True, + default='', encrypted=True, label=_('Logging Aggregator Password/Token'), help_text=_('Password or authentication token for external log aggregator (if required).'), From 2432a3d3c3264e6086fa175781a8e96b67313182 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 13 Feb 2017 16:16:43 -0500 Subject: [PATCH 078/260] Revert "remove partial dependency job id logic" This reverts commit 4bb3a4909e616209fc291b2b3cee46469bc58f9e. --- awx/main/scheduler/partial.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/awx/main/scheduler/partial.py b/awx/main/scheduler/partial.py index 6cba9c35b8..6cb87add15 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,35 @@ 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: + 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 @@ -90,7 +113,8 @@ class JobDict(PartialModelDict): kv = { 'status__in': status } - return [cls(o) for o in cls.model.objects.filter(**kv).values(*cls.get_db_values())] + merged = PartialModelDict.merge_values(cls.model.objects.filter(**kv).values(*cls.get_db_values())) + return [cls(o) for o in merged] class ProjectUpdateDict(PartialModelDict): From 770580b612c326b70c99a3bffd97a6bb13b5ba3c Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 13 Feb 2017 14:46:54 -0500 Subject: [PATCH 079/260] special case for user capability with null WFJT organization --- awx/main/access.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/main/access.py b/awx/main/access.py index e45eb932bc..acbf704d45 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -343,6 +343,9 @@ class BaseAccess(object): if validation_errors: user_capabilities[display_method] = False continue + elif display_method == 'copy' and isinstance(obj, WorkflowJobTemplate) and obj.organization_id is None: + user_capabilities[display_method] = self.user.is_superuser + continue elif display_method in ['start', 'schedule'] and isinstance(obj, Group): if obj.inventory_source and not obj.inventory_source._can_update(): user_capabilities[display_method] = False From d1a71fb7bee5e9b26f9af53f07d490b415159aac Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Sun, 12 Feb 2017 16:13:15 -0500 Subject: [PATCH 080/260] add supervisor option to development environment --- .gitignore | 1 + Makefile | 6 ++ tools/docker-compose/Dockerfile | 1 + tools/docker-compose/start_development.sh | 8 ++- tools/docker-compose/supervisor.conf | 73 +++++++++++++++++++++++ 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 tools/docker-compose/supervisor.conf diff --git a/.gitignore b/.gitignore index ca9dd12298..d5e58e8c87 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,4 @@ local/ awx/lib/.deps_built awx/lib/site-packages venv/* +use_dev_supervisor.txt diff --git a/Makefile b/Makefile index 6d55edc698..836f96da9c 100644 --- a/Makefile +++ b/Makefile @@ -378,6 +378,12 @@ server: server_noattach servercc: server_noattach tmux -2 -CC attach-session -t tower +supervisor: + @if [ "$(VENV_BASE)" ]; then \ + . $(VENV_BASE)/tower/bin/activate; \ + fi; \ + supervisord --configuration /supervisor.conf --pidfile=/tmp/supervisor_pid + # Alternate approach to tmux to run all development tasks specified in # Procfile. https://youtu.be/OPMgaibszjk honcho: diff --git a/tools/docker-compose/Dockerfile b/tools/docker-compose/Dockerfile index 19b699ab36..4a78226a3a 100644 --- a/tools/docker-compose/Dockerfile +++ b/tools/docker-compose/Dockerfile @@ -19,6 +19,7 @@ RUN mkdir -p /etc/tower RUN mkdir -p /data/db ADD tools/docker-compose/license /etc/tower/license RUN pip2 install honcho +RUN pip2 install supervisor RUN curl -LO https://github.com/Yelp/dumb-init/releases/download/v1.1.3/dumb-init_1.1.3_amd64 && chmod +x ./dumb-init_1.1.3_amd64 && mv ./dumb-init_1.1.3_amd64 /usr/bin/dumb-init ADD tools/docker-compose/ansible-tower.egg-link /tmp/ansible-tower.egg-link ADD tools/docker-compose/tower-manage /usr/local/bin/tower-manage diff --git a/tools/docker-compose/start_development.sh b/tools/docker-compose/start_development.sh index ee94888431..9814a9344c 100755 --- a/tools/docker-compose/start_development.sh +++ b/tools/docker-compose/start_development.sh @@ -25,6 +25,7 @@ fi cp -nR /tmp/ansible_tower.egg-info /tower_devel/ || true cp /tmp/ansible-tower.egg-link /venv/tower/lib/python2.7/site-packages/ansible-tower.egg-link +yes | cp -rf /tower_devel/tools/docker-compose/supervisor.conf /supervisor.conf # Tower bootstrapping make version_file @@ -35,4 +36,9 @@ mkdir -p /tower_devel/awx/public/static mkdir -p /tower_devel/awx/ui/static # Start the service -make honcho + +if [ -f "/tower_devel/tools/docker-compose/use_dev_supervisor.txt" ]; then + make supervisor +else + make honcho +fi diff --git a/tools/docker-compose/supervisor.conf b/tools/docker-compose/supervisor.conf new file mode 100644 index 0000000000..787720fec3 --- /dev/null +++ b/tools/docker-compose/supervisor.conf @@ -0,0 +1,73 @@ +[supervisord] +umask = 022 +minfds = 4096 +nodaemon=true + +[program:celeryd] +command = make celeryd +autostart = true +autorestart = true +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:receiver] +command = make receiver +autostart = true +autorestart = true +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:runworker] +command = make runworker +autostart = true +autorestart = true +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:uwsgi] +command = make uwsgi +autostart = true +autorestart = true +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:daphne] +command = make daphne +autostart = true +autorestart = true +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:factcacher] +command = make factcacher +autostart = true +autorestart = true +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:nginx] +command = make nginx +autostart = true +autorestart = true +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:flower] +command = make flower +autostart = true +autorestart = true +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[group:tower-processes] +programs=celeryd,receiver,runworker,uwsgi,daphne,factcacher,nginx,flower +priority=5 + From b5aad8cbed9a6c56d31034c8b584d44f53fb5672 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 13 Feb 2017 11:45:11 -0500 Subject: [PATCH 081/260] bypass the makefile target for most commands in dev supervisor --- tools/docker-compose/supervisor.conf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/docker-compose/supervisor.conf b/tools/docker-compose/supervisor.conf index 787720fec3..f33066e627 100644 --- a/tools/docker-compose/supervisor.conf +++ b/tools/docker-compose/supervisor.conf @@ -4,7 +4,7 @@ minfds = 4096 nodaemon=true [program:celeryd] -command = make celeryd +command = python manage.py celeryd -l DEBUG -B --autoreload --autoscale=20,3 --schedule=/celerybeat-schedule -Q projects,jobs,default,scheduler,broadcast_all,%(ENV_HOSTNAME)s -n celery@%(ENV_HOSTNAME)s autostart = true autorestart = true redirect_stderr=true @@ -12,7 +12,7 @@ stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 [program:receiver] -command = make receiver +command = python manage.py run_callback_receiver autostart = true autorestart = true redirect_stderr=true @@ -20,7 +20,7 @@ stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 [program:runworker] -command = make runworker +command = python manage.py runworker --only-channels websocket.* autostart = true autorestart = true redirect_stderr=true @@ -36,7 +36,7 @@ stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 [program:daphne] -command = make daphne +command = daphne -b 0.0.0.0 -p 8051 awx.asgi:channel_layer autostart = true autorestart = true redirect_stderr=true @@ -44,7 +44,7 @@ stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 [program:factcacher] -command = make factcacher +command = python manage.py run_fact_cache_receiver autostart = true autorestart = true redirect_stderr=true @@ -52,7 +52,7 @@ stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 [program:nginx] -command = make nginx +command = nginx -g "daemon off;" autostart = true autorestart = true redirect_stderr=true From 9a880910425a80b9be486cd59f1490e93f618f70 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 13 Feb 2017 17:33:28 -0500 Subject: [PATCH 082/260] get supervisorctl to work in dev supervisor env --- tools/docker-compose/supervisor.conf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/docker-compose/supervisor.conf b/tools/docker-compose/supervisor.conf index f33066e627..aab7d8aeb7 100644 --- a/tools/docker-compose/supervisor.conf +++ b/tools/docker-compose/supervisor.conf @@ -71,3 +71,11 @@ stdout_logfile_maxbytes=0 programs=celeryd,receiver,runworker,uwsgi,daphne,factcacher,nginx,flower priority=5 +[unix_http_server] +file=/tmp/supervisor.sock + +[supervisorctl] +serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface From 0e6e116fa3b73d95df7b26c643f1f2e2cbfa7343 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 13 Feb 2017 19:02:10 -0500 Subject: [PATCH 083/260] Explicitly setting an order_by fixes the issue of workflow nodes coming back out of whack on page 2 and beyond --- awx/ui/client/src/workflow-results/workflow-results.route.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cbf8778b34..168f0b8329 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.route.js +++ b/awx/ui/client/src/workflow-results/workflow-results.route.js @@ -49,7 +49,7 @@ export default { // flashing as rest data comes in. Provides the list of workflow nodes workflowNodes: ['workflowData', 'Rest', '$q', function(workflowData, Rest, $q) { var defer = $q.defer(); - Rest.setUrl(workflowData.related.workflow_nodes); + Rest.setUrl(workflowData.related.workflow_nodes + '?order_by=id'); Rest.get() .success(function(data) { if(data.next) { From 409090789c5b0af4aa3314e47806916db8a97f05 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 13 Feb 2017 20:00:30 -0500 Subject: [PATCH 084/260] Add l10n files to data_files in setup.py --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d7b2e925b0..9d3a674089 100755 --- a/setup.py +++ b/setup.py @@ -117,7 +117,9 @@ setup( }, data_files = proc_data_files([ ("%s" % homedir, ["config/wsgi.py", - "awx/static/favicon.ico"]), + "awx/static/favicon.ico", + "awx/locale/**/*.po", + "awx/locale/**/*.mo"]), ("%s" % siteconfig, ["config/awx-nginx.conf"]), # ("%s" % webconfig, ["config/uwsgi_params"]), ("%s" % sharedir, ["tools/scripts/request_tower_configuration.sh","tools/scripts/request_tower_configuration.ps1"]), From 7282e4c3328b0675c38c63007e083e4411d400f9 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 13 Feb 2017 20:24:16 -0500 Subject: [PATCH 085/260] Fix globbing pattern for l10n files TIL **/* only searches direct child directories on Linux, but is recursive on macOS --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 9d3a674089..334b0e78d5 100755 --- a/setup.py +++ b/setup.py @@ -118,8 +118,8 @@ setup( data_files = proc_data_files([ ("%s" % homedir, ["config/wsgi.py", "awx/static/favicon.ico", - "awx/locale/**/*.po", - "awx/locale/**/*.mo"]), + "awx/locale/*/LC_MESSAGES/*.po", + "awx/locale/*/LC_MESSAGES/*.mo"]), ("%s" % siteconfig, ["config/awx-nginx.conf"]), # ("%s" % webconfig, ["config/uwsgi_params"]), ("%s" % sharedir, ["tools/scripts/request_tower_configuration.sh","tools/scripts/request_tower_configuration.ps1"]), From c3eff539d00e2815f1fe0306d4104bf3c6e9f95f Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 14 Feb 2017 08:59:02 -0500 Subject: [PATCH 086/260] fixes for unified_jobs related m2m search listing --- awx/api/generics.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 001b4b0305..e3bcdc221c 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -282,13 +282,17 @@ class ListAPIView(generics.ListAPIView, GenericAPIView): fields.append('{}__search'.format(field.name)) for rel in self.model._meta.related_objects: name = rel.get_accessor_name() + if name is None: + continue if name.endswith('_set'): continue fields.append('{}__search'.format(name)) m2m_rel = [] m2m_rel += self.model._meta.local_many_to_many - if issubclass(self.model, UnifiedJobTemplate): + if issubclass(self.model, UnifiedJobTemplate) and self.model != UnifiedJobTemplate: m2m_rel += UnifiedJobTemplate._meta.local_many_to_many + if issubclass(self.model, UnifiedJob) and self.model != UnifiedJob: + m2m_rel += UnifiedJob._meta.local_many_to_many for relationship in m2m_rel: if relationship.related_model._meta.app_label != 'main': continue From 57a4d60d587fef378a871b78dd2c98388944097a Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 14 Feb 2017 10:14:32 -0500 Subject: [PATCH 087/260] Tweaked org job templates list query params --- .../organizations/linkout/organizations-linkout.route.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 ca98e3b388..da1b2eb3ca 100644 --- a/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js +++ b/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js @@ -234,7 +234,8 @@ export default [{ params: { template_search: { value: { - project__organization: null + or__project__organization: null, + or__inventory__organization: null } } }, @@ -279,7 +280,8 @@ export default [{ OrgJobTemplateDataset: ['OrgJobTemplateList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { let path = GetBasePath(list.name); - $stateParams.template_search.project__organization = $stateParams.organization_id; + $stateParams.template_search.or__project__organization = $stateParams.organization_id; + $stateParams.template_search.or__inventory__organization = $stateParams.organization_id; return qs.search(path, $stateParams.template_search); } ] From a36ce01537e5b4ff9408d9846f010beb025e9554 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 14 Feb 2017 10:56:09 -0500 Subject: [PATCH 088/260] adding options callback for project type labels --- .../rbac-multiselect-list.directive.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) 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 aebe4c94bb..c03e7fb275 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 @@ -123,6 +123,36 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL _.forEach(scope[`${list.name}`], isSelected); }); + scope.$on(`${list.iterator}_options`, function(event, data){ + scope.options = data.data.actions.GET; + 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(){ + if(scope.list.name === 'projects'){ + if (scope[list.name] !== undefined) { + 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; + }); + } + + }); + } + } + } + function isSelected(item){ if(_.find(scope.allSelected, {id: item.id, type: item.type})){ item.isSelected = true; From e41bbf5ebea4b48df1eb683527362e20dc547558 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 14 Feb 2017 11:26:52 -0500 Subject: [PATCH 089/260] Fixed org templates pagination by setting page_size --- .../src/organizations/linkout/organizations-linkout.route.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 da1b2eb3ca..729cb99bbe 100644 --- a/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js +++ b/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js @@ -235,7 +235,8 @@ export default [{ template_search: { value: { or__project__organization: null, - or__inventory__organization: null + or__inventory__organization: null, + page_size: 20 } } }, From cae8950723dbcde382fb5be300fc9c0d71c99cf1 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 14 Feb 2017 10:59:46 -0500 Subject: [PATCH 090/260] don't cache social-auth-core backends social-auth-core uses a global variable to cache backend settings: https://github.com/python-social-auth/social-core/blob/78da4eb201dd22fd2d8a4e38a1d17a73beabad24/social_core/backends/utils.py#L9 when loading backends, forcibly ignore this behavior to avoid a thread-safety issue that causes #4788 #4045 --- 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 56d5e7d789..97a90428ce 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -518,7 +518,7 @@ class AuthView(APIView): def get(self, request): data = OrderedDict() err_backend, err_message = request.session.get('social_auth_error', (None, None)) - auth_backends = load_backends(settings.AUTHENTICATION_BACKENDS).items() + auth_backends = load_backends(settings.AUTHENTICATION_BACKENDS, force_load=True).items() # Return auth backends in consistent order: Google, GitHub, SAML. auth_backends.sort(key=lambda x: 'g' if x[0] == 'google-oauth2' else x[0]) for name, backend in auth_backends: From ff531088c90dfe57b43d5044eff202d2d421e08e Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Tue, 14 Feb 2017 11:46:02 -0500 Subject: [PATCH 091/260] Mark some stuff for translation. Markup courtesy cargo-cults-r-us. For all your 'code changes without understanding' needs. --- .../src/access/add-rbac-resource/rbac-resource.partial.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html index b664f81378..cc2c3afbba 100644 --- a/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html +++ b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html @@ -62,7 +62,7 @@ Please assign roles to the selected users/teams
+ ng-click="toggleKeyPane()" translate> Key
@@ -104,13 +104,13 @@
From 8b33557633d7c4e53bcb99cd9651d53b360a713c Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Tue, 14 Feb 2017 11:58:02 -0500 Subject: [PATCH 092/260] More cargo-culty translation markup --- awx/ui/client/src/partials/inventory-add.html | 2 +- awx/ui/client/src/partials/subhome.html | 14 +++++------ .../src/partials/survey-maker-modal.html | 24 +++++++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/awx/ui/client/src/partials/inventory-add.html b/awx/ui/client/src/partials/inventory-add.html index a0eaa60be7..7b8c6f9144 100644 --- a/awx/ui/client/src/partials/inventory-add.html +++ b/awx/ui/client/src/partials/inventory-add.html @@ -8,7 +8,7 @@
What would you like to name the copy of job template ?
-
Please enter a name for this job template copy.
+
Please enter a name for this job template copy.
diff --git a/awx/ui/client/src/partials/subhome.html b/awx/ui/client/src/partials/subhome.html index 626c7e4b23..035553e815 100644 --- a/awx/ui/client/src/partials/subhome.html +++ b/awx/ui/client/src/partials/subhome.html @@ -4,11 +4,11 @@ diff --git a/awx/ui/client/src/partials/survey-maker-modal.html b/awx/ui/client/src/partials/survey-maker-modal.html index 7ee3974129..b34c621f81 100644 --- a/awx/ui/client/src/partials/survey-maker-modal.html +++ b/awx/ui/client/src/partials/survey-maker-modal.html @@ -10,12 +10,12 @@ @@ -23,8 +23,8 @@
{{name || "New Job Template"}}
SURVEY
-
ON
-
OFF
+
ON
+
OFF
@@ -40,9 +40,9 @@
-
PREVIEW
+
PREVIEW
-
PLEASE ADD A SURVEY PROMPT ON THE LEFT.
+
PLEASE ADD A SURVEY PROMPT ON THE LEFT.
  • @@ -73,17 +73,17 @@
-
  • +
  • Drop question here to reorder
  • - - - - + + + +
    From da981f413fbead6632c3e8a4fc9b295d5af28f5b Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 14 Feb 2017 12:00:29 -0500 Subject: [PATCH 093/260] hack to get firefox to make the pane height correct --- awx/ui/client/src/job-results/job-results.block.less | 2 ++ 1 file changed, 2 insertions(+) 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 f48b86948a..679cb10a77 100644 --- a/awx/ui/client/src/job-results/job-results.block.less +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -180,6 +180,8 @@ smart-search { job-results-standard-out { flex: 1; + flex-basis: auto; + height: ~"calc(100% - 800px)"; display: flex } From 8eaaf37825ccd1041e059226d9a2b977650127dc Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Tue, 14 Feb 2017 13:07:12 -0500 Subject: [PATCH 094/260] Updated top margin for empty lists --- awx/ui/client/legacy-styles/lists.less | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index 37e34e2515..d293e80872 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -278,6 +278,7 @@ table, tbody { } .List-noItems { + margin-top: 52px; display: flex; align-items: center; justify-content: center; From d054f866527c9d7bb4905e05b32372d792330617 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 14 Feb 2017 11:35:58 -0500 Subject: [PATCH 095/260] update organization counts to correspond with UI lists --- awx/api/views.py | 39 ++++++++++--------- .../api/test_organization_counts.py | 23 +++++++++++ 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 56d5e7d789..ccd45916d5 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -22,7 +22,7 @@ from django.contrib.auth.models import User, AnonymousUser from django.core.cache import cache from django.core.urlresolvers import reverse from django.core.exceptions import FieldError -from django.db.models import Q, Count +from django.db.models import Q, Count, F from django.db import IntegrityError, transaction, connection from django.shortcuts import get_object_or_404 from django.utils.encoding import smart_text, force_text @@ -646,15 +646,16 @@ class OrganizationCountsMixin(object): self.request.user, 'read_role').values('organization').annotate( Count('organization')).order_by('organization') - JT_reference = 'project__organization' - db_results['job_templates'] = JobTemplate.accessible_objects( - self.request.user, 'read_role').exclude(job_type='scan').values(JT_reference).annotate( - Count(JT_reference)).order_by(JT_reference) + JT_project_reference = 'project__organization' + JT_inventory_reference = 'inventory__organization' + db_results['job_templates_project'] = JobTemplate.accessible_objects( + self.request.user, 'read_role').exclude( + project__organization=F(JT_inventory_reference)).values(JT_project_reference).annotate( + Count(JT_project_reference)).order_by(JT_project_reference) - JT_scan_reference = 'inventory__organization' - db_results['job_templates_scan'] = JobTemplate.accessible_objects( - self.request.user, 'read_role').filter(job_type='scan').values(JT_scan_reference).annotate( - Count(JT_scan_reference)).order_by(JT_scan_reference) + db_results['job_templates_inventory'] = JobTemplate.accessible_objects( + self.request.user, 'read_role').values(JT_inventory_reference).annotate( + Count(JT_inventory_reference)).order_by(JT_inventory_reference) db_results['projects'] = project_qs\ .values('organization').annotate(Count('organization')).order_by('organization') @@ -672,16 +673,16 @@ class OrganizationCountsMixin(object): 'inventories': 0, 'teams': 0, 'users': 0, 'job_templates': 0, 'admins': 0, 'projects': 0} - for res in db_results: - if res == 'job_templates': - org_reference = JT_reference - elif res == 'job_templates_scan': - org_reference = JT_scan_reference + for res, count_qs in db_results.items(): + if res == 'job_templates_project': + org_reference = JT_project_reference + elif res == 'job_templates_inventory': + org_reference = JT_inventory_reference elif res == 'users': org_reference = 'id' else: org_reference = 'organization' - for entry in db_results[res]: + for entry in count_qs: org_id = entry[org_reference] if org_id in count_context: if res == 'users': @@ -690,11 +691,13 @@ class OrganizationCountsMixin(object): continue count_context[org_id][res] = entry['%s__count' % org_reference] - # Combine the counts for job templates with scan job templates + # Combine the counts for job templates by project and inventory for org in org_id_list: org_id = org['id'] - if 'job_templates_scan' in count_context[org_id]: - count_context[org_id]['job_templates'] += count_context[org_id].pop('job_templates_scan') + count_context[org_id]['job_templates'] = 0 + for related_path in ['job_templates_project', 'job_templates_inventory']: + if related_path in count_context[org_id]: + count_context[org_id]['job_templates'] += count_context[org_id].pop(related_path) full_context['related_field_counts'] = count_context diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py index 500667e107..f08fd75d01 100644 --- a/awx/main/tests/functional/api/test_organization_counts.py +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -181,6 +181,29 @@ def test_scan_JT_counted(resourced_organization, user, get): assert detail_response.data['summary_fields']['related_field_counts'] == counts_dict +@pytest.mark.django_db +def test_JT_not_double_counted(resourced_organization, user, get): + admin_user = user('admin', True) + # Add a scan job template to the org + resourced_organization.projects.all()[0].jobtemplates.create( + job_type='run', + inventory=resourced_organization.inventories.all()[0], + project=resourced_organization.projects.all()[0], + name='double-linked-job-template') + counts_dict = COUNTS_PRIMES + counts_dict['job_templates'] += 1 + + # Test list view + list_response = get(reverse('api:organization_list', args=[]), admin_user) + assert list_response.status_code == 200 + assert list_response.data['results'][0]['summary_fields']['related_field_counts'] == counts_dict + + # Test detail view + detail_response = get(reverse('api:organization_detail', args=[resourced_organization.pk]), admin_user) + assert detail_response.status_code == 200 + assert detail_response.data['summary_fields']['related_field_counts'] == counts_dict + + @pytest.mark.django_db def test_JT_associated_with_project(organizations, project, user, get): # Check that adding a project to an organization gets the project's JT From 8553b8eda6d8a27c7e514730e3f4b040b73dbbf1 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 14 Feb 2017 14:21:27 -0500 Subject: [PATCH 096/260] Ignore hipchat certificate verification --- awx/main/notifications/hipchat_backend.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/notifications/hipchat_backend.py b/awx/main/notifications/hipchat_backend.py index 586754bd92..b286439954 100644 --- a/awx/main/notifications/hipchat_backend.py +++ b/awx/main/notifications/hipchat_backend.py @@ -37,6 +37,7 @@ class HipChatBackend(TowerBaseEmailBackend): for rcp in m.recipients(): r = requests.post("{}/v2/room/{}/notification".format(self.api_url, rcp), params={"auth_token": self.token}, + verify=False, json={"color": self.color, "message": m.subject, "notify": self.notify, From f2d840021ffe45d29c3d788f6f2e00c5fe216f9c Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Tue, 14 Feb 2017 14:23:01 -0500 Subject: [PATCH 097/260] Updated modal action buttons to be above search --- awx/ui/client/legacy-styles/lists.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index 37e34e2515..1de3ee8eed 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -433,7 +433,7 @@ table, tbody { } } -.InventoryManage-container { +.InventoryManage-container, .modal-body { .List-header { flex-direction: column; align-items: stretch; From 576984922d52ef26c7460dd3dca52add28dad26a Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 14 Feb 2017 14:36:19 -0500 Subject: [PATCH 098/260] auditing .every() and replacing with .forEach() --- .../rbac-multiselect-list.directive.js | 10 ++++------ awx/ui/client/src/controllers/Credentials.js | 6 ++---- awx/ui/client/src/controllers/Projects.js | 8 ++------ awx/ui/client/src/helpers/Jobs.js | 8 ++------ awx/ui/client/src/jobs/jobs-list.controller.js | 4 +--- .../notification-templates-list/list.controller.js | 4 +--- .../organizations-job-templates.controller.js | 4 +--- .../controllers/organizations-projects.controller.js | 8 ++------ .../src/templates/list/templates-list.controller.js | 4 +--- 9 files changed, 16 insertions(+), 40 deletions(-) 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 c03e7fb275..16594c01a9 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 @@ -138,15 +138,13 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL // 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) { + scope.options.hasOwnProperty('scm_type')) { + scope.options.scm_type.choices.forEach(function(choice) { + if (choice[0] === item.scm_type) { itm.type_label = choice[1]; - return false; } - return true; }); - } + } }); } diff --git a/awx/ui/client/src/controllers/Credentials.js b/awx/ui/client/src/controllers/Credentials.js index 3c06b69182..93b0e5909c 100644 --- a/awx/ui/client/src/controllers/Credentials.js +++ b/awx/ui/client/src/controllers/Credentials.js @@ -56,12 +56,10 @@ export function CredentialsList($scope, $rootScope, $location, $log, // Set the item type label if (list.fields.kind && $scope.options && $scope.options.hasOwnProperty('kind')) { - $scope.options.kind.choices.every(function(choice) { + $scope.options.kind.choices.forEach(function(choice) { if (choice[0] === item.kind) { itm.kind_label = choice[1]; - return false; } - return true; }); } }); @@ -462,7 +460,7 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, form: form, reset: false }); - + master.kind = $scope.kind; CreateSelect2({ diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 4b4ef05c19..e97194c1b1 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -58,12 +58,10 @@ export function ProjectsList($scope, $rootScope, $location, $log, $stateParams, // 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) { + $scope.options.scm_type.choices.forEach(function(choice) { if (choice[0] === item.scm_type) { itm.type_label = choice[1]; - return false; } - return true; }); } @@ -275,7 +273,7 @@ export function ProjectsList($scope, $rootScope, $location, $log, $stateParams, } catch (e) { // ignore } - $scope.projects.every(function(project) { + $scope.projects.forEach(function(project) { if (project.id === project_id) { if (project.scm_type === "Manual" || Empty(project.scm_type)) { // Do not respond. Button appears greyed out as if it is disabled. Not disabled though, because we need mouse over event @@ -286,9 +284,7 @@ export function ProjectsList($scope, $rootScope, $location, $log, $stateParams, } else { ProjectUpdate({ scope: $scope, project_id: project.id }); } - return false; } - return true; }); }; diff --git a/awx/ui/client/src/helpers/Jobs.js b/awx/ui/client/src/helpers/Jobs.js index 13a0198bb7..ff5cdfead1 100644 --- a/awx/ui/client/src/helpers/Jobs.js +++ b/awx/ui/client/src/helpers/Jobs.js @@ -88,21 +88,17 @@ export default // Set the item type label if (list.fields.type) { - parent_scope.type_choices.every(function(choice) { + parent_scope.type_choices.forEach(function(choice) { if (choice.value === item.type) { itm.type_label = choice.label; - return false; } - return true; }); } // Set the job status label - parent_scope.status_choices.every(function(status) { + parent_scope.status_choices.forEach(function(status) { if (status.value === item.status) { itm.status_label = status.label; - return false; } - return true; }); if (list.name === 'completed_jobs' || list.name === 'running_jobs') { diff --git a/awx/ui/client/src/jobs/jobs-list.controller.js b/awx/ui/client/src/jobs/jobs-list.controller.js index d590cf7e17..ac1ef684cd 100644 --- a/awx/ui/client/src/jobs/jobs-list.controller.js +++ b/awx/ui/client/src/jobs/jobs-list.controller.js @@ -58,12 +58,10 @@ // Set the item type label if (list.fields.type && $scope.options && $scope.options.hasOwnProperty('type')) { - $scope.options.type.choices.every(function(choice) { + $scope.options.type.choices.forEach(function(choice) { if (choice[0] === item.type) { itm.type_label = choice[1]; - return false; } - return true; }); } buildTooltips(itm); 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 d3aec68845..867c0ad914 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 @@ -50,14 +50,12 @@ // 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) { + $scope.options.notification_type.choices.forEach(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); diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js index e42d67659d..3f81b1550b 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js @@ -60,12 +60,10 @@ export default ['$scope', '$rootScope', '$location', '$log', // Set the item type label if (list.fields.type && $scope.options && $scope.options.hasOwnProperty('type')) { - $scope.options.type.choices.every(function(choice) { + $scope.options.type.choices.forEach(function(choice) { if (choice[0] === item.type) { itm.type_label = choice[1]; - return false; } - return true; }); } }); diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js index c43e50aa15..7032ef5f81 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js @@ -85,12 +85,10 @@ export default ['$scope', '$rootScope', '$location', '$log', // 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) { + $scope.options.scm_type.choices.forEach(function(choice) { if (choice[0] === item.scm_type) { itm.type_label = choice[1]; - return false; } - return true; }); } @@ -301,7 +299,7 @@ export default ['$scope', '$rootScope', '$location', '$log', } catch (e) { // ignore } - $scope.projects.every(function(project) { + $scope.projects.forEach(function(project) { if (project.id === project_id) { if (project.scm_type === "Manual" || Empty(project.scm_type)) { // Do not respond. Button appears greyed out as if it is disabled. Not disabled though, because we need mouse over event @@ -312,9 +310,7 @@ export default ['$scope', '$rootScope', '$location', '$log', } else { ProjectUpdate({ scope: $scope, project_id: project.id }); } - return false; } - return true; }); }; 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 d3e07c6fda..5fc6695ca2 100644 --- a/awx/ui/client/src/templates/list/templates-list.controller.js +++ b/awx/ui/client/src/templates/list/templates-list.controller.js @@ -59,12 +59,10 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', // Set the item type label if (list.fields.type && $scope.options.hasOwnProperty('type')) { - $scope.options.type.choices.every(function(choice) { + $scope.options.type.choices.forEach(function(choice) { if (choice[0] === item.type) { itm.type_label = choice[1]; - return false; } - return true; }); } }); From 30595470f7383ea71c5c27e48130fd6763abda9c Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 14 Feb 2017 11:17:30 -0500 Subject: [PATCH 099/260] Set project/playbook to defaults when null --- awx/ui/client/src/helpers/JobTemplates.js | 3 +-- .../edit-job-template/job-template-edit.controller.js | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/awx/ui/client/src/helpers/JobTemplates.js b/awx/ui/client/src/helpers/JobTemplates.js index 921d5b1e37..31687df847 100644 --- a/awx/ui/client/src/helpers/JobTemplates.js +++ b/awx/ui/client/src/helpers/JobTemplates.js @@ -155,8 +155,7 @@ angular.module('JobTemplatesHelper', ['Utilities']) scope.can_edit = data.summary_fields.user_capabilities.edit; - - if (scope.project === "" && scope.playbook === "") { + if ((!scope.project || scope.project === "") && (!scope.playbook || scope.playbook === "")) { scope.resetProjectToDefault(); } diff --git a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js index 8312c09543..59f1ccd310 100644 --- a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js +++ b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js @@ -53,10 +53,6 @@ export default $scope.parseType = 'yaml'; $scope.showJobType = false; - if($scope.job_type && $scope.job_type.value === 'scan' && !$scope.project) { - $scope.project_name = 'Default'; - } - SurveyControllerInit({ scope: $scope, parent_scope: $scope, @@ -270,6 +266,7 @@ export default var dft; master = masterObject; + getPlaybooks($scope.project); dft = ($scope.host_config_key === "" || $scope.host_config_key === null) ? false : true; From e17a4c4712f2385e48334f959b67a2d9d8d868cc Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 14 Feb 2017 15:11:00 -0500 Subject: [PATCH 100/260] fix management schedule number inputs --- .../management-jobs/scheduler/schedulerForm.partial.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html b/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html index e4302004df..304aaa33c2 100644 --- a/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html +++ b/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html @@ -529,7 +529,7 @@
    - +
    @@ -545,8 +545,8 @@
    - +
    From 758aafec11a3d6c97beb405963945d1f2544f074 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 14 Feb 2017 14:55:29 -0500 Subject: [PATCH 101/260] Fixed jt/wfjt links in activity stream --- awx/ui/client/src/widgets/Stream.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/awx/ui/client/src/widgets/Stream.js b/awx/ui/client/src/widgets/Stream.js index 1fc1a40520..64315e7953 100644 --- a/awx/ui/client/src/widgets/Stream.js +++ b/awx/ui/client/src/widgets/Stream.js @@ -80,6 +80,12 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti break; case 'role': throw {name : 'NotImplementedError', message : 'role object management is not consolidated to a single UI view'}; + case 'job_template': + url += `templates/job_template/${obj.id}`; + break; + case 'workflow_job_template': + url += `templates/workflow_job_template/${obj.id}`; + break; default: url += resource + 's/' + obj.id + '/'; } From ee8c82df4a261689a26546ae4407f4c26c63f6a9 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 14 Feb 2017 15:18:03 -0500 Subject: [PATCH 102/260] fix a test that fails due to a race between async logging requests --- awx/main/tests/unit/utils/test_handlers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/main/tests/unit/utils/test_handlers.py b/awx/main/tests/unit/utils/test_handlers.py index 2e0fcbe8d5..3de3b2e7b7 100644 --- a/awx/main/tests/unit/utils/test_handlers.py +++ b/awx/main/tests/unit/utils/test_handlers.py @@ -213,8 +213,9 @@ def test_https_logging_handler_emit_one_record_per_fact(ok200_adapter): [future.result() for future in async_futures] assert len(ok200_adapter.requests) == 2 + requests = sorted(ok200_adapter.requests, key=lambda request: json.loads(request.body)['version']) - request = ok200_adapter.requests[0] + request = requests[0] assert request.url == 'http://127.0.0.1/' assert request.method == 'POST' body = json.loads(request.body) @@ -223,7 +224,7 @@ def test_https_logging_handler_emit_one_record_per_fact(ok200_adapter): assert body['name'] == 'ansible' assert body['version'] == '2.2.1.0' - request = ok200_adapter.requests[1] + request = requests[1] assert request.url == 'http://127.0.0.1/' assert request.method == 'POST' body = json.loads(request.body) From be686e0accb9e8facc652a559b1fb39a8508c97d Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 14 Feb 2017 15:37:52 -0500 Subject: [PATCH 103/260] Tweak logic used by UI to detect language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chromium and WebKit based browsers have the window.navigator.languages attribute, which is an ordered array of preferred languages as configured in the browser’s settings. Although changing the language in Chrome results in an Accept-Language header being added to requests, window.navigator.language still returns the language specified by the OS. I’ve tested this with Firefox, Chrome, IE 11, and Edge. Connect #5360 --- awx/ui/client/src/i18n.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/i18n.js b/awx/ui/client/src/i18n.js index b37d05a8c0..d024f01c7d 100644 --- a/awx/ui/client/src/i18n.js +++ b/awx/ui/client/src/i18n.js @@ -19,7 +19,8 @@ export default .factory('I18NInit', ['$window', 'gettextCatalog', function ($window, gettextCatalog) { return function() { - var langInfo = $window.navigator.language || + var langInfo = ($window.navigator.languages || [])[0] || + $window.navigator.language || $window.navigator.userLanguage; var langUrl = langInfo.replace('-', '_'); //gettextCatalog.debug = true; From 3bc4d515d7fba7000aa0328db3acc249dc537523 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 14 Feb 2017 15:46:24 -0500 Subject: [PATCH 104/260] fix disabling stuff --- .../scheduler/schedulerForm.partial.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html b/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html index 304aaa33c2..581e34d0c2 100644 --- a/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html +++ b/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html @@ -529,7 +529,7 @@
    - +
    @@ -546,7 +546,7 @@
    + ng-required="true" min="0" max="9999" integer ng-disabled="!(schedule_obj.summary_fields.user_capabilities.edit || canAdd)" aw-min="0" aw-max="9999" required>
    @@ -562,13 +562,13 @@
    + ng-show="(scheduler_form.$invalid || !schedulerIsValid) && scheduler_form.$dirty">

    The scheduler options are invalid or incomplete.

    + ng-show="!scheduler_form.$invalid && schedulerIsValid"> @@ -642,7 +642,7 @@ id="project_save_btn" ng-click="saveSchedule()" ng-show="(schedule_obj.summary_fields.user_capabilities.edit || canAdd)" - ng-disabled="!schedulerIsValid"> Save + ng-disabled="scheduler_form.$invalid || !schedulerIsValid"> Save
    From 045994a47227b7c959fedae38e526c1a10cebeb7 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 14 Feb 2017 16:18:51 -0500 Subject: [PATCH 105/260] properly detect the backend name in failed social_auth callbacks don't assume that the callback URL contains the correct social_auth backend name; instead, store it temporarily in the session at `/login/sso/` see: #5324 --- awx/sso/middleware.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/awx/sso/middleware.py b/awx/sso/middleware.py index 012bcefd55..c678ff08f3 100644 --- a/awx/sso/middleware.py +++ b/awx/sso/middleware.py @@ -23,6 +23,10 @@ from awx.main.models import AuthToken class SocialAuthMiddleware(SocialAuthExceptionMiddleware): + def process_view(self, request, callback, callback_args, callback_kwargs): + if request.path.startswith('/sso/login/'): + request.session['social_auth_last_backend'] = callback_kwargs['backend'] + def process_request(self, request): token_key = request.COOKIES.get('token', '') token_key = urllib.quote(urllib.unquote(token_key).strip('"')) @@ -57,6 +61,7 @@ class SocialAuthMiddleware(SocialAuthExceptionMiddleware): if auth_token and request.user and request.user.is_authenticated(): request.session.pop('social_auth_error', None) + request.session.pop('social_auth_last_backend', None) def process_exception(self, request, exception): strategy = getattr(request, 'social_strategy', None) @@ -66,6 +71,12 @@ class SocialAuthMiddleware(SocialAuthExceptionMiddleware): if isinstance(exception, SocialAuthBaseException) or request.path.startswith('/sso/'): backend = getattr(request, 'backend', None) backend_name = getattr(backend, 'name', 'unknown-backend') + + message = self.get_message(request, exception) + if request.session.get('social_auth_last_backend') != backend_name: + backend_name = request.session.get('social_auth_last_backend') + message = request.GET.get('error_description', message) + full_backend_name = backend_name try: idp_name = strategy.request_data()['RelayState'] @@ -73,7 +84,6 @@ class SocialAuthMiddleware(SocialAuthExceptionMiddleware): except KeyError: pass - message = self.get_message(request, exception) social_logger.error(message) url = self.get_redirect_uri(request, exception) From 065bd6041cc1504824e7aa499ec27b6ad069ba85 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 14 Feb 2017 16:54:32 -0500 Subject: [PATCH 106/260] Default to empty string for UI language detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When removing all “Languages” from Firefox, the UI breaks and you see an error in the console that says “langInfo is undefined”. This fixes that. --- awx/ui/client/src/i18n.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/i18n.js b/awx/ui/client/src/i18n.js index d024f01c7d..2b24a09822 100644 --- a/awx/ui/client/src/i18n.js +++ b/awx/ui/client/src/i18n.js @@ -21,7 +21,8 @@ export default return function() { var langInfo = ($window.navigator.languages || [])[0] || $window.navigator.language || - $window.navigator.userLanguage; + $window.navigator.userLanguage || + ''; var langUrl = langInfo.replace('-', '_'); //gettextCatalog.debug = true; gettextCatalog.setCurrentLanguage(langInfo); From 28f3f178f00b084dd375bebac7635fa293a96633 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 14 Feb 2017 16:59:30 -0500 Subject: [PATCH 107/260] only allow single selection for LOG_AGGREGATOR_TYPE see: #5000 --- .../system-form/configuration-system.controller.js | 2 +- .../configuration/system-form/sub-forms/system-logging.form.js | 1 - 2 files changed, 1 insertion(+), 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 3a3a1b789d..05af8fbfc8 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 @@ -167,7 +167,7 @@ export default [ dropdownRendered = true; CreateSelect2({ element: '#configuration_logging_template_LOG_AGGREGATOR_TYPE', - multiple: true, + multiple: false, placeholder: i18n._('Select types'), opts: opts }); 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 index f2ed9f54e3..9669d8074a 100644 --- 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 @@ -23,7 +23,6 @@ type: 'select', reset: 'LOG_AGGREGATOR_TYPE', ngOptions: 'type.label for type in LOG_AGGREGATOR_TYPE_options track by type.value', - multiSelect: true }, LOG_AGGREGATOR_USERNAME: { type: 'text', From c44f4526dff83735a1aa97afa1c40aa689057fbd Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 14 Feb 2017 15:11:26 -0500 Subject: [PATCH 108/260] removing breadcrumb from permission modal states --- awx/ui/client/src/shared/stateDefinitions.factory.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index f24ec4993d..6a3e530401 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -272,6 +272,9 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto dynamic: true } }, + ncyBreadcrumb:{ + skip:true + }, views: { [`modal@${formStateDefinition.name}`]: { template: `` @@ -342,6 +345,9 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto template: `` } }, + ncyBreadcrumb:{ + skip:true + }, resolve: { usersDataset: ['addPermissionsUsersList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { @@ -511,6 +517,9 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto template: `` } }, + ncyBreadcrumb:{ + skip:true + }, resolve: { usersDataset: ['addPermissionsUsersList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { From b89edcac1634ab314034db9b190eaa59b014186e Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 14 Feb 2017 20:58:07 -0500 Subject: [PATCH 109/260] relaunching on jobs page should always keep user on jobs page, for all job types --- .../launchjob.factory.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js index f0063054fb..c7bc2d499f 100644 --- a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js +++ b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js @@ -128,17 +128,14 @@ export default $state.go(state, {id: job}, {reload:true}); }; - if(_.has(data, 'job')) { - if(base === 'jobs'){ - if(scope.clearDialog) { - scope.clearDialog(); - } - return; + if(base === 'jobs'){ + if(scope.clearDialog) { + scope.clearDialog(); } - else{ - goToJobDetails('jobDetail'); - } - + return; + } + else if(_.has(data, 'job')) { + goToJobDetails('jobDetail'); } else if(data.type && data.type === 'workflow_job') { job = data.id; From cd35d64118b20ee1d660f1e4607d795979e62f21 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Wed, 15 Feb 2017 06:53:20 -0500 Subject: [PATCH 110/260] Ensure WorkflowJobWorkflowNodesList orders correctly, default by PK --- awx/api/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/api/views.py b/awx/api/views.py index 56d5e7d789..982bc197d6 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -44,6 +44,7 @@ from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import exception_handler from rest_framework import status +from rest_framework import filters # Django REST Framework YAML from rest_framework_yaml.parsers import YAMLParser @@ -3148,6 +3149,9 @@ class WorkflowJobWorkflowNodesList(WorkflowsEnforcementMixin, SubListAPIView): relationship = 'workflow_job_nodes' parent_key = 'workflow_job' new_in_310 = True + filter_backends = (filters.OrderingFilter,) + order_fields = ('id', 'job', 'workflow_job', 'unified_job_template', 'created', 'modified') + ordering = ('id',) class WorkflowJobCancel(WorkflowsEnforcementMixin, RetrieveAPIView): From d8f133d1756e4ceff3d10c61f9b2bdbff548dfe0 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 14 Feb 2017 17:13:48 -0500 Subject: [PATCH 111/260] Add supervisor command to restart select services on reload event --- awx/main/tasks.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index d3d12b6259..458a4a022e 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -10,6 +10,7 @@ import json import logging import os import signal +import subprocess import pipes import re import shutil @@ -105,13 +106,38 @@ def _uwsgi_reload(): awxfifo.write(TRIGGER_CHAIN_RELOAD) -def _reset_celery_logging(): +def _reset_celery_thread_pool(): # Send signal to restart thread pool app = current_app._get_current_object() app.control.broadcast('pool_restart', arguments={'reload': True}, destination=['celery@{}'.format(settings.CLUSTER_HOST_ID)], reply=False) +def _supervisor_service_restart(): + ''' + example use pattern of supervisorctl: + # supervisorctl restart tower-processes:receiver tower-processes:factcacher + ''' + group_name = 'tower-processes' + args = ['supervisorctl'] + if settings.DEBUG is True: + args.extend(['-c', '/supervisor.conf']) + programs = "receiver,factcacher".split(",") + else: + programs = "awx-celeryd-beat,awx-callback-receiver,awx-fact-cache-receiver".split(",") + args.extend(['restart']) + args.extend(['{}:{}'.format(group_name, p) for p in programs]) + logger.debug('Issuing command to restart services, args={}'.format(args)) + subprocess.Popen(args) + + +def restart_local_services(): + logger.warn('Restarting services on this node in response to user action') + _uwsgi_reload() + _supervisor_service_restart() + _reset_celery_thread_pool() + + def _clear_cache_keys(set_of_keys): logger.debug('cache delete_many(%r)', set_of_keys) cache.delete_many(set_of_keys) @@ -125,8 +151,7 @@ def process_cache_changes(cache_keys): _clear_cache_keys(set_of_keys) for setting_key in set_of_keys: if setting_key.startswith('LOG_AGGREGATOR_'): - _uwsgi_reload() - _reset_celery_logging() + restart_local_services() break From ded8eea5e7ea68b7247f9e6a073a0f1f70209c53 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Wed, 15 Feb 2017 09:49:16 -0500 Subject: [PATCH 112/260] Added docs link --- awx/ui/client/src/shared/smart-search/smart-search.partial.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/shared/smart-search/smart-search.partial.html b/awx/ui/client/src/shared/smart-search/smart-search.partial.html index b19b011698..2e151244ce 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.partial.html +++ b/awx/ui/client/src/shared/smart-search/smart-search.partial.html @@ -51,7 +51,7 @@ RELATED FIELDS: {{ relation }},
    - ADDITIONAL INFORMATION: For additional information on advanced search search syntax please see the Ansible Tower documentation. + ADDITIONAL INFORMATION: For additional information on advanced search search syntax please see the Ansible Tower documentation.
    From 4d9bcbfaea5b9a15d75809b8f645f82e4fc696c7 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 15 Feb 2017 10:34:53 -0500 Subject: [PATCH 113/260] nvd3 1.7.1 needs version 3.3.13 of d3.js in order for the tooltip guideline to work --- awx/ui/npm-shrinkwrap.json | 6 +++--- awx/ui/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/ui/npm-shrinkwrap.json b/awx/ui/npm-shrinkwrap.json index faec63cc41..2f1127a0cb 100644 --- a/awx/ui/npm-shrinkwrap.json +++ b/awx/ui/npm-shrinkwrap.json @@ -1391,9 +1391,9 @@ "dev": true }, "d3": { - "version": "3.5.17", - "from": "d3@>=3.3.13 <4.0.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz" + "version": "3.3.13", + "from": "d3@3.3.13", + "resolved": "https://registry.npmjs.org/d3/-/d3-3.3.13.tgz" }, "dashdash": { "version": "1.14.1", diff --git a/awx/ui/package.json b/awx/ui/package.json index 7095b616a4..3dbf77e777 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -94,7 +94,7 @@ "bootstrap-datepicker": "^1.4.0", "codemirror": "^5.17.0", "components-font-awesome": "^4.6.1", - "d3": "^3.3.13", + "d3": "3.3.13", "javascript-detect-element-resize": "^0.5.3", "jquery": "2.2.4", "jquery-ui": "1.10.5", From c459e326b29d379a97896a4cbadfe843f65698cc Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 13 Feb 2017 14:55:34 -0500 Subject: [PATCH 114/260] UI work to incorporate related_search_fields as valid fields when searching --- .../smart-search/django-search-model.class.js | 53 +++++-------------- .../shared/smart-search/queryset.service.js | 45 ++++++---------- .../smart-search/smart-search.controller.js | 27 ++++------ .../smart-search/smart-search.partial.html | 2 +- 4 files changed, 43 insertions(+), 84 deletions(-) diff --git a/awx/ui/client/src/shared/smart-search/django-search-model.class.js b/awx/ui/client/src/shared/smart-search/django-search-model.class.js index 5271a38a30..81984e662e 100644 --- a/awx/ui/client/src/shared/smart-search/django-search-model.class.js +++ b/awx/ui/client/src/shared/smart-search/django-search-model.class.js @@ -1,28 +1,3 @@ -// Ignored fields are not surfaced in the UI's search key -let isIgnored = function(key, value) { - let ignored = [ - 'type', - 'url', - 'related', - 'summary_fields', - 'object_roles', - 'activity_stream', - 'update', - 'teams', - 'users', - 'owner_teams', - 'owner_users', - 'access_list', - 'notification_templates_error', - 'notification_templates_success', - 'ad_hoc_command_events', - 'fact_versions', - 'variable_data', - 'playbooks' - ]; - return ignored.indexOf(key) > -1 || value.type === 'field'; -}; - export default class DjangoSearchModel { /* @@ -36,21 +11,21 @@ class DjangoSearchModel { } @@property related ['field' ...] */ - constructor(name, endpoint, baseFields, relations) { - let base = {}; + constructor(name, baseFields, relatedSearchFields) { + function trimRelated(relatedSearchField){ + return relatedSearchField.replace(/\__search$/, ""); + } this.name = name; - this.related = _.reject(relations, isIgnored); - _.forEach(baseFields, (value, key) => { - if (!isIgnored(key, value)) { - base[key] = value; + this.related = _.map(relatedSearchFields, trimRelated); + // Remove "object" type fields from this list + for (var key in baseFields) { + if (baseFields.hasOwnProperty(key)) { + if (baseFields[key].type === 'object'){ + delete baseFields[key]; + } } - }); - this.base = base; - } - - fields() { - let result = this.base; - result.related = this.related; - return result; + } + delete baseFields.url; + this.base = baseFields; } } 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 10ae7ef3c8..35f2b088f1 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -1,42 +1,31 @@ -export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory', 'SmartSearchService', - function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory, SmartSearchService) { +export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', 'SmartSearchService', + function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, SmartSearchService) { return { // kick off building a model for a specific endpoint // this is usually a list's basePath // unified_jobs is the exception, where we need to fetch many subclass OPTIONS and summary_fields - initFieldset(path, name, relations) { - // get or set $cachFactory.Cache object with id '$http' - let defer = $q.defer(), - cache = $cacheFactory.get('$http') || $cacheFactory('$http'); - defer.resolve(this.getCommonModelOptions(path, name, relations, cache)); + initFieldset(path, name) { + let defer = $q.defer(); + defer.resolve(this.getCommonModelOptions(path, name)); return defer.promise; }, - getCommonModelOptions(path, name, relations, cache) { + getCommonModelOptions(path, name) { let resolve, base, defer = $q.defer(); - // grab a single model from the cache, if present - if (cache.get(path)) { - 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({ - models: { - [name]: new DjangoSearchModel(name, path, base, relations) - }, - options: res - }); + this.url = path; + resolve = this.options(path) + .then((res) => { + base = res.data.actions.GET; + let relatedSearchFields = res.data.related_search_fields; + defer.resolve({ + models: { + [name]: new DjangoSearchModel(name, base, relatedSearchFields) + }, + options: res }); - } + }); return defer.promise; }, 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 bbac64b37d..506613c125 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 @@ -1,7 +1,7 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet', 'SmartSearchService', 'i18n', function($stateParams, $scope, $state, QuerySet, GetBasePath, qs, SmartSearchService, i18n) { - let path, relations, + let path, defaults, queryset, stateChangeSuccessListener; @@ -28,9 +28,8 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' function init() { 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((data) => { + qs.initFieldset(path, $scope.djangoModel).then((data) => { $scope.models = data.models; $scope.options = data.options.data; $scope.$emit(`${$scope.list.iterator}_options`, data.options); @@ -107,14 +106,6 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' return _(strippedCopy).map(qs.decodeParam).flatten().value(); } - // searchable relationships - function getRelationshipFields(dataset) { - let flat = _(dataset).map((value) => { - return _.keys(value.related); - }).flatten().uniq().value(); - return flat; - } - function setDefaults(term) { if ($scope.list.defaultSearchParams) { return $scope.list.defaultSearchParams(term); @@ -175,8 +166,8 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' let encodeParams = { term: tagToRemove }; - if(_.has($scope.options.actions.GET, root)) { - if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') { + if(_.has($scope.models[$scope.list.name].base, root)) { + if($scope.models[$scope.list.name].base[root].type && $scope.models[$scope.list.name].base[root].type === 'field') { encodeParams.relatedSearchTerm = true; } else { @@ -184,6 +175,10 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' } removed = qs.encodeParam(encodeParams); } + else if(_.contains($scope.models[$scope.list.name].related, root)) { + encodeParams.relatedSearchTerm = true; + removed = qs.encodeParam(encodeParams); + } else { removed = setDefaults(termParts[termParts.length-1]); } @@ -241,8 +236,8 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' } else { // Figure out if this is a search term let root = termParts[0].split(".")[0].replace(/^-/, ''); - if(_.has($scope.options.actions.GET, root)) { - if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') { + if(_.has($scope.models[$scope.list.name].base, root)) { + if($scope.models[$scope.list.name].base[root].type && $scope.models[$scope.list.name].base[root].type === 'field') { params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches); } else { @@ -252,7 +247,7 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' // The related fields need to also be checked for related searches. // The related fields for the search are retrieved from the API // options endpoint, and are stored in the $scope.model. FYI, the - // Django search model is what sets the related fields on the model. + // Django search model is what sets the related fields on the model. else if(_.contains($scope.models[$scope.list.name].related, root)) { params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches); } diff --git a/awx/ui/client/src/shared/smart-search/smart-search.partial.html b/awx/ui/client/src/shared/smart-search/smart-search.partial.html index b19b011698..13a5562aaa 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.partial.html +++ b/awx/ui/client/src/shared/smart-search/smart-search.partial.html @@ -47,7 +47,7 @@
    FIELDS: {{ key }},
    -
    +
    RELATED FIELDS: {{ relation }},
    From b3662ecdd68dcb0c1d0b68ee7ff50345bef80c10 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 15 Feb 2017 11:23:52 -0500 Subject: [PATCH 115/260] Run through optionsRequestDataProcessing whenever the list data changes --- .../access/rbac-multiselect/rbac-multiselect-list.directive.js | 1 + 1 file changed, 1 insertion(+) 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 16594c01a9..8f3d924ec2 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 @@ -121,6 +121,7 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL scope.$watch(list.name, function(){ _.forEach(scope[`${list.name}`], isSelected); + optionsRequestDataProcessing(); }); scope.$on(`${list.iterator}_options`, function(event, data){ From 186b672e4f6f4cf864ffc361586d23a75a822a32 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 15 Feb 2017 12:07:30 -0500 Subject: [PATCH 116/260] move service definition into settings --- awx/main/tasks.py | 43 ++++++++++++++---------- awx/main/tests/unit/utils/test_reload.py | 13 +++++++ awx/settings/development.py | 12 +++++++ awx/settings/production.py | 12 +++++++ 4 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 awx/main/tests/unit/utils/test_reload.py diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 458a4a022e..937a247b10 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -98,11 +98,7 @@ def _uwsgi_reload(): # http://uwsgi-docs.readthedocs.io/en/latest/MasterFIFO.html#available-commands logger.warn('Initiating uWSGI chain reload of server') TRIGGER_CHAIN_RELOAD = 'c' - if settings.DEBUG: - uWSGI_FIFO_LOCATION = '/awxfifo' - else: - uWSGI_FIFO_LOCATION = '/var/lib/awx/awxfifo' - with open(uWSGI_FIFO_LOCATION, 'w') as awxfifo: + with open(settings.uWSGI_FIFO_LOCATION, 'w') as awxfifo: awxfifo.write(TRIGGER_CHAIN_RELOAD) @@ -113,29 +109,42 @@ def _reset_celery_thread_pool(): destination=['celery@{}'.format(settings.CLUSTER_HOST_ID)], reply=False) -def _supervisor_service_restart(): +def _supervisor_service_restart(service_internal_names): ''' + Service internal name options: + - beat - celery - callback - channels - uwsgi - daphne + - fact - nginx example use pattern of supervisorctl: # supervisorctl restart tower-processes:receiver tower-processes:factcacher ''' group_name = 'tower-processes' args = ['supervisorctl'] - if settings.DEBUG is True: + if settings.DEBUG: args.extend(['-c', '/supervisor.conf']) - programs = "receiver,factcacher".split(",") - else: - programs = "awx-celeryd-beat,awx-callback-receiver,awx-fact-cache-receiver".split(",") + programs = [] + name_translation_dict = settings.SERVICE_NAME_DICT + for n in service_internal_names: + if n in name_translation_dict: + programs.append('{}:{}'.format(group_name, name_translation_dict[n])) args.extend(['restart']) - args.extend(['{}:{}'.format(group_name, p) for p in programs]) + args.extend(programs) logger.debug('Issuing command to restart services, args={}'.format(args)) subprocess.Popen(args) -def restart_local_services(): - logger.warn('Restarting services on this node in response to user action') - _uwsgi_reload() - _supervisor_service_restart() - _reset_celery_thread_pool() +def restart_local_services(service_internal_names): + logger.warn('Restarting services {} on this node in response to user action'.format(service_internal_names)) + if 'uwsgi' in service_internal_names: + _uwsgi_reload() + service_internal_names.pop('uwsgi') + restart_celery = False + if 'celery' in service_internal_names: + restart_celery = True + service_internal_names.pop('celery') + _supervisor_service_restart(service_internal_names) + if restart_celery: + # Celery restarted last because this probably includes current process + _reset_celery_thread_pool() def _clear_cache_keys(set_of_keys): @@ -151,7 +160,7 @@ def process_cache_changes(cache_keys): _clear_cache_keys(set_of_keys) for setting_key in set_of_keys: if setting_key.startswith('LOG_AGGREGATOR_'): - restart_local_services() + restart_local_services(['uwsgi', 'celery', 'beat', 'callback', 'fact']) break diff --git a/awx/main/tests/unit/utils/test_reload.py b/awx/main/tests/unit/utils/test_reload.py new file mode 100644 index 0000000000..555f09eec5 --- /dev/null +++ b/awx/main/tests/unit/utils/test_reload.py @@ -0,0 +1,13 @@ +# from django.conf import LazySettings +import pytest + +# awx.main.utils.reload +from awx.main.main.tasks import _supervisor_service_restart, subprocess + + +def test_produce_supervisor_command(mocker): + with mocker.patch.object(subprocess, 'Popen'): + _supervisor_service_restart(['beat', 'callback', 'fact']) + subprocess.Popen.assert_called_once_with( + ['supervisorctl', 'restart', 'tower-processes:receiver', 'tower-processes:factcacher']) + diff --git a/awx/settings/development.py b/awx/settings/development.py index 1326c12814..23f79f7c60 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -112,3 +112,15 @@ except ImportError: CLUSTER_HOST_ID = socket.gethostname() CELERY_ROUTES['awx.main.tasks.cluster_node_heartbeat'] = {'queue': CLUSTER_HOST_ID, 'routing_key': CLUSTER_HOST_ID} +# Supervisor service name dictionary used for programatic restart +SERVICE_NAME_DICT = { + "celery": "celeryd", + "callback": "receiver", + "runworker": "channels", + "uwsgi": "uwsgi", + "daphne": "daphne", + "fact": "factcacher", + "nginx": "nginx"} +# Used for sending commands in automatic restart +uWSGI_FIFO_LOCATION = '/awxfifo' + diff --git a/awx/settings/production.py b/awx/settings/production.py index f056a4ea31..92e5e6e81e 100644 --- a/awx/settings/production.py +++ b/awx/settings/production.py @@ -57,6 +57,18 @@ 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' +# Supervisor service name dictionary used for programatic restart +SERVICE_NAME_DICT = { + "beat": "awx-celeryd-beat", + "celery": "awx-celeryd", + "callback": "awx-callback-receiver", + "channels": "awx-channels-worker", + "uwsgi": "awx-uwsgi", + "daphne": "awx-daphne", + "fact": "awx-fact-cache-receiver"} +# Used for sending commands in automatic restart +uWSGI_FIFO_LOCATION = '/var/lib/awx/awxfifo' + # Store a snapshot of default settings at this point before loading any # customizable config files. DEFAULTS_SNAPSHOT = {} From ac2f063c89c43aaa26a34cd6a60f13fda6a6a25f Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 15 Feb 2017 12:20:22 -0500 Subject: [PATCH 117/260] fixing codemirror instantiation on ctit auth-form --- .../configuration-auth.controller.js | 66 +++++++++++-------- .../configuration/configuration.controller.js | 8 ++- 2 files changed, 45 insertions(+), 29 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 a674eb76aa..e4d471d9ba 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 @@ -60,9 +60,11 @@ export default [ } var activeForm = function() { + if(!$scope.$parent[formTracker.currentFormName()].$dirty) { authVm.activeAuthForm = authVm.dropdownValue; formTracker.setCurrentAuth(authVm.activeAuthForm); + startCodeMirrors(); } else { var msg = i18n._('You have unsaved changes. Would you like to proceed without saving?'); var title = i18n._('Warning: Unsaved Changes'); @@ -115,28 +117,36 @@ export default [ var authForms = [{ formDef: configurationAzureForm, - id: 'auth-azure-form' + id: 'auth-azure-form', + name: 'azure' }, { formDef: configurationGithubForm, - id: 'auth-github-form' + id: 'auth-github-form', + name: 'github' }, { formDef: configurationGithubOrgForm, - id: 'auth-github-org-form' + id: 'auth-github-org-form', + name: 'github_org' }, { formDef: configurationGithubTeamForm, - id: 'auth-github-team-form' + id: 'auth-github-team-form', + name: 'github_team' }, { formDef: configurationGoogleForm, - id: 'auth-google-form' + id: 'auth-google-form', + name: 'google_oauth' }, { formDef: configurationLdapForm, - id: 'auth-ldap-form' + id: 'auth-ldap-form', + name: 'ldap' }, { formDef: configurationRadiusForm, - id: 'auth-radius-form' + id: 'auth-radius-form', + name: 'radius' }, { formDef: configurationSamlForm, - id: 'auth-saml-form' + id: 'auth-saml-form', + name: 'saml' }, ]; var forms = _.pluck(authForms, 'formDef'); @@ -161,6 +171,27 @@ export default [ form.buttons.save.disabled = $rootScope.user_is_system_auditor; }); + function startCodeMirrors(){ + // Attach codemirror to fields that need it + let form = _.find(authForms, function(form){ + return form.name === $scope.authVm.activeAuthForm; + }); + _.each(form.formDef.fields, function(field) { + // Codemirror balks at empty values so give it one + if($scope.$parent[field.name] === null && field.codeMirror) { + $scope.$parent[field.name] = '{}'; + } + if(field.codeMirror) { + ParseTypeChange({ + scope: $scope.$parent, + variable: field.name, + parse_variable: 'parseType', + field_id: form.formDef.name + '_' + field.name + }); + } + }); + } + function addFieldInfo(form, key) { _.extend(form.fields[key], { awPopOver: ($scope.$parent.configDataResolve[key].defined_in_file) ? @@ -195,24 +226,7 @@ export default [ var dropdownRendered = false; $scope.$on('populated', function() { - // Attach codemirror to fields that need it - _.each(authForms, function(form) { - _.each(form.formDef.fields, function(field) { - // Codemirror balks at empty values so give it one - if($scope.$parent[field.name] === null && field.codeMirror) { - $scope.$parent[field.name] = '{}'; - } - if(field.codeMirror) { - ParseTypeChange({ - scope: $scope.$parent, - variable: field.name, - parse_variable: 'parseType', - field_id: form.formDef.name + '_' + field.name, - readonly: true, - }); - } - }); - }); + startCodeMirrors(); // Create Select2 fields var opts = []; diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js index f9319300a9..78b9b228d3 100644 --- a/awx/ui/client/src/configuration/configuration.controller.js +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -7,7 +7,7 @@ export default [ '$scope', '$rootScope', '$state', '$stateParams', '$timeout', '$q', 'Alert', 'ClearScope', 'ConfigurationService', 'ConfigurationUtils', 'CreateDialog', 'CreateSelect2', 'i18n', 'ParseTypeChange', 'ProcessErrors', 'Store', - 'Wait', 'configDataResolve', + 'Wait', 'configDataResolve', 'ToJSON', //Form definitions 'configurationAzureForm', 'configurationGithubForm', @@ -25,7 +25,7 @@ export default [ function( $scope, $rootScope, $state, $stateParams, $timeout, $q, Alert, ClearScope, ConfigurationService, ConfigurationUtils, CreateDialog, CreateSelect2, i18n, ParseTypeChange, ProcessErrors, Store, - Wait, configDataResolve, + Wait, configDataResolve, ToJSON, //Form definitions configurationAzureForm, configurationGithubForm, @@ -363,7 +363,9 @@ export default [ if($scope[key] === '') { payload[key] = {}; } else { - payload[key] = JSON.parse($scope[key]); + // payload[key] = JSON.parse($scope[key]); + payload[key] = ToJSON($scope.parseType, + $scope[key]); } } else { From 4cbdeb0d3059d7b20c333e6f1410ea305202ec05 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 15 Feb 2017 12:22:52 -0500 Subject: [PATCH 118/260] don't allow private key passphrases for unencrypted private ssh keys see: #5311 --- awx/main/models/credential.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index a7f77e87c2..3342c8b750 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -345,6 +345,9 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): if self.has_encrypted_ssh_key_data and not self.ssh_key_unlock: raise ValidationError(_('SSH key unlock must be set when SSH key ' 'is encrypted.')) + if not self.has_encrypted_ssh_key_data and self.ssh_key_unlock: + raise ValidationError(_('SSH key unlock should not be set when ' + 'SSH key is not encrypted.')) return self.ssh_key_unlock def clean(self): From 7e3a5fd2c26056a87c29eb4ef1b63fcbb854651a Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 15 Feb 2017 13:01:36 -0500 Subject: [PATCH 119/260] move reload functionality to its own file --- awx/main/tasks.py | 56 +------------------ awx/main/tests/unit/utils/test_reload.py | 38 ++++++++++--- awx/main/utils/reload.py | 68 ++++++++++++++++++++++++ awx/settings/development.py | 2 +- awx/settings/production.py | 2 +- 5 files changed, 102 insertions(+), 64 deletions(-) create mode 100644 awx/main/utils/reload.py diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 937a247b10..9791e15ccf 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -10,7 +10,6 @@ import json import logging import os import signal -import subprocess import pipes import re import shutil @@ -34,7 +33,6 @@ import pexpect # Celery from celery import Task, task from celery.signals import celeryd_init, worker_process_init -from celery import current_app # Django from django.conf import settings @@ -55,6 +53,7 @@ from awx.main.task_engine import TaskEnhancer from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url, check_proot_installed, build_proot_temp_dir, wrap_args_with_proot, get_system_task_capacity, OutputEventFilter, parse_yaml_or_json) +from awx.main.utils.reload import restart_local_services from awx.main.utils.handlers import configure_external_logger from awx.main.consumers import emit_channel_notification @@ -94,59 +93,6 @@ def task_set_logger_pre_run(*args, **kwargs): configure_external_logger(settings, async_flag=False, is_startup=False) -def _uwsgi_reload(): - # 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(settings.uWSGI_FIFO_LOCATION, 'w') as awxfifo: - awxfifo.write(TRIGGER_CHAIN_RELOAD) - - -def _reset_celery_thread_pool(): - # Send signal to restart thread pool - app = current_app._get_current_object() - app.control.broadcast('pool_restart', arguments={'reload': True}, - destination=['celery@{}'.format(settings.CLUSTER_HOST_ID)], reply=False) - - -def _supervisor_service_restart(service_internal_names): - ''' - Service internal name options: - - beat - celery - callback - channels - uwsgi - daphne - - fact - nginx - example use pattern of supervisorctl: - # supervisorctl restart tower-processes:receiver tower-processes:factcacher - ''' - group_name = 'tower-processes' - args = ['supervisorctl'] - if settings.DEBUG: - args.extend(['-c', '/supervisor.conf']) - programs = [] - name_translation_dict = settings.SERVICE_NAME_DICT - for n in service_internal_names: - if n in name_translation_dict: - programs.append('{}:{}'.format(group_name, name_translation_dict[n])) - args.extend(['restart']) - args.extend(programs) - logger.debug('Issuing command to restart services, args={}'.format(args)) - subprocess.Popen(args) - - -def restart_local_services(service_internal_names): - logger.warn('Restarting services {} on this node in response to user action'.format(service_internal_names)) - if 'uwsgi' in service_internal_names: - _uwsgi_reload() - service_internal_names.pop('uwsgi') - restart_celery = False - if 'celery' in service_internal_names: - restart_celery = True - service_internal_names.pop('celery') - _supervisor_service_restart(service_internal_names) - if restart_celery: - # Celery restarted last because this probably includes current process - _reset_celery_thread_pool() - - def _clear_cache_keys(set_of_keys): logger.debug('cache delete_many(%r)', set_of_keys) cache.delete_many(set_of_keys) diff --git a/awx/main/tests/unit/utils/test_reload.py b/awx/main/tests/unit/utils/test_reload.py index 555f09eec5..3b8d66b56d 100644 --- a/awx/main/tests/unit/utils/test_reload.py +++ b/awx/main/tests/unit/utils/test_reload.py @@ -1,13 +1,37 @@ -# from django.conf import LazySettings -import pytest - # awx.main.utils.reload -from awx.main.main.tasks import _supervisor_service_restart, subprocess +from awx.main.utils import reload def test_produce_supervisor_command(mocker): - with mocker.patch.object(subprocess, 'Popen'): - _supervisor_service_restart(['beat', 'callback', 'fact']) - subprocess.Popen.assert_called_once_with( + with mocker.patch.object(reload.subprocess, 'Popen'): + reload._supervisor_service_restart(['beat', 'callback', 'fact']) + reload.subprocess.Popen.assert_called_once_with( ['supervisorctl', 'restart', 'tower-processes:receiver', 'tower-processes:factcacher']) + +def test_routing_of_service_restarts_works(mocker): + ''' + This tests that the parent restart method will call the appropriate + service restart methods, depending on which services are given in args + ''' + with mocker.patch.object(reload, '_uwsgi_reload'): + with mocker.patch.object(reload, '_reset_celery_thread_pool'): + with mocker.patch.object(reload, '_supervisor_service_restart'): + reload.restart_local_services(['uwsgi', 'celery', 'flower', 'daphne']) + reload._uwsgi_reload.assert_called_once_with() + reload._reset_celery_thread_pool.assert_called_once_with() + reload._supervisor_service_restart.assert_called_once_with(['flower', 'daphne']) + + +def test_routing_of_service_restarts_diables(mocker): + ''' + Test that methods are not called if not in the args + ''' + with mocker.patch.object(reload, '_uwsgi_reload'): + with mocker.patch.object(reload, '_reset_celery_thread_pool'): + with mocker.patch.object(reload, '_supervisor_service_restart'): + reload.restart_local_services(['flower']) + reload._uwsgi_reload.assert_not_called() + reload._reset_celery_thread_pool.assert_not_called() + reload._supervisor_service_restart.assert_called_once_with(['flower']) + diff --git a/awx/main/utils/reload.py b/awx/main/utils/reload.py new file mode 100644 index 0000000000..729a33a703 --- /dev/null +++ b/awx/main/utils/reload.py @@ -0,0 +1,68 @@ +# Copyright (c) 2017 Ansible Tower by Red Hat +# All Rights Reserved. + +# Python +import subprocess +import logging + +# Django +from django.conf import settings + +# Celery +from celery import current_app + +logger = logging.getLogger('awx.main.utils.reload') + + +def _uwsgi_reload(): + # 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(settings.UWSGI_FIFO_LOCATION, 'w') as awxfifo: + awxfifo.write(TRIGGER_CHAIN_RELOAD) + + +def _reset_celery_thread_pool(): + # Send signal to restart thread pool + app = current_app._get_current_object() + app.control.broadcast('pool_restart', arguments={'reload': True}, + destination=['celery@{}'.format(settings.CLUSTER_HOST_ID)], reply=False) + + +def _supervisor_service_restart(service_internal_names): + ''' + Service internal name options: + - beat - celery - callback - channels - uwsgi - daphne + - fact - nginx + example use pattern of supervisorctl: + # supervisorctl restart tower-processes:receiver tower-processes:factcacher + ''' + group_name = 'tower-processes' + args = ['supervisorctl'] + if settings.DEBUG: + args.extend(['-c', '/supervisor.conf']) + programs = [] + name_translation_dict = settings.SERVICE_NAME_DICT + for n in service_internal_names: + if n in name_translation_dict: + programs.append('{}:{}'.format(group_name, name_translation_dict[n])) + args.extend(['restart']) + args.extend(programs) + logger.debug('Issuing command to restart services, args={}'.format(args)) + subprocess.Popen(args) + + +def restart_local_services(service_internal_names): + logger.warn('Restarting services {} on this node in response to user action'.format(service_internal_names)) + if 'uwsgi' in service_internal_names: + _uwsgi_reload() + service_internal_names.remove('uwsgi') + restart_celery = False + if 'celery' in service_internal_names: + restart_celery = True + service_internal_names.remove('celery') + _supervisor_service_restart(service_internal_names) + if restart_celery: + # Celery restarted last because this probably includes current process + _reset_celery_thread_pool() + diff --git a/awx/settings/development.py b/awx/settings/development.py index 23f79f7c60..0a0bc748f2 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -122,5 +122,5 @@ SERVICE_NAME_DICT = { "fact": "factcacher", "nginx": "nginx"} # Used for sending commands in automatic restart -uWSGI_FIFO_LOCATION = '/awxfifo' +UWSGI_FIFO_LOCATION = '/awxfifo' diff --git a/awx/settings/production.py b/awx/settings/production.py index 92e5e6e81e..19afcab9c9 100644 --- a/awx/settings/production.py +++ b/awx/settings/production.py @@ -67,7 +67,7 @@ SERVICE_NAME_DICT = { "daphne": "awx-daphne", "fact": "awx-fact-cache-receiver"} # Used for sending commands in automatic restart -uWSGI_FIFO_LOCATION = '/var/lib/awx/awxfifo' +UWSGI_FIFO_LOCATION = '/var/lib/awx/awxfifo' # Store a snapshot of default settings at this point before loading any # customizable config files. From 785a8d0789c6fc35de2cef6fcfdb374de2fd02c1 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 15 Feb 2017 14:05:58 -0500 Subject: [PATCH 120/260] Fix an issue where smtplib can't handle unicode strings We probably do get this value as unicode originally but when we store it, due to a recently fixed bug it will come out as *not* unicode. So things were accidentally working because py2 smtplib uses hmac which won't accept unicode. This change adds a flag to encrypt_field that forces it to skip the utf8 fixup from before for narrow use cases. --- awx/main/models/notifications.py | 2 +- awx/main/tests/unit/utils/common/test_common.py | 8 ++++++++ awx/main/utils/common.py | 7 +++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index 8ba92b3782..31b96aa8dd 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -75,7 +75,7 @@ class NotificationTemplate(CommonModel): setattr(self, '_saved_{}_{}'.format("config", field), value) self.notification_configuration[field] = '' else: - encrypted = encrypt_field(self, 'notification_configuration', subfield=field) + encrypted = encrypt_field(self, 'notification_configuration', subfield=field, skip_utf8=True) self.notification_configuration[field] = encrypted if 'notification_configuration' not in update_fields: update_fields.append('notification_configuration') diff --git a/awx/main/tests/unit/utils/common/test_common.py b/awx/main/tests/unit/utils/common/test_common.py index a48bbe64b3..6542d64cf0 100644 --- a/awx/main/tests/unit/utils/common/test_common.py +++ b/awx/main/tests/unit/utils/common/test_common.py @@ -29,6 +29,14 @@ def test_encrypt_field_with_unicode_string(): assert common.decrypt_field(field, 'value') == value +def test_encrypt_field_force_disable_unicode(): + value = u"NothingSpecial" + field = Setting(value=value) + encrypted = field.value = common.encrypt_field(field, 'value', skip_utf8=True) + assert "UTF8" not in encrypted + assert common.decrypt_field(field, 'value') == value + + def test_encrypt_subfield(): field = Setting(value={'name': 'ANSIBLE'}) encrypted = field.value = common.encrypt_field(field, 'value', subfield='name') diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index e49f4d0131..49d92b5f9c 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -183,7 +183,7 @@ def get_encryption_key(field_name, pk=None): return h.digest()[:16] -def encrypt_field(instance, field_name, ask=False, subfield=None): +def encrypt_field(instance, field_name, ask=False, subfield=None, skip_utf8=False): ''' Return content of the given instance and field name encrypted. ''' @@ -192,7 +192,10 @@ def encrypt_field(instance, field_name, ask=False, subfield=None): value = value[subfield] if not value or value.startswith('$encrypted$') or (ask and value == 'ASK'): return value - utf8 = type(value) == six.text_type + if skip_utf8: + utf8 = False + else: + utf8 = type(value) == six.text_type value = smart_str(value) key = get_encryption_key(field_name, getattr(instance, 'pk', None)) cipher = AES.new(key, AES.MODE_ECB) From 3023b4dfaa6289265c5e3f04c08a47540071ab2b Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 15 Feb 2017 14:53:01 -0500 Subject: [PATCH 121/260] switch to new nested with pattern from PR review --- awx/main/tests/unit/utils/test_reload.py | 29 ++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/awx/main/tests/unit/utils/test_reload.py b/awx/main/tests/unit/utils/test_reload.py index 3b8d66b56d..d1f3291753 100644 --- a/awx/main/tests/unit/utils/test_reload.py +++ b/awx/main/tests/unit/utils/test_reload.py @@ -14,24 +14,25 @@ def test_routing_of_service_restarts_works(mocker): This tests that the parent restart method will call the appropriate service restart methods, depending on which services are given in args ''' - with mocker.patch.object(reload, '_uwsgi_reload'): - with mocker.patch.object(reload, '_reset_celery_thread_pool'): - with mocker.patch.object(reload, '_supervisor_service_restart'): - reload.restart_local_services(['uwsgi', 'celery', 'flower', 'daphne']) - reload._uwsgi_reload.assert_called_once_with() - reload._reset_celery_thread_pool.assert_called_once_with() - reload._supervisor_service_restart.assert_called_once_with(['flower', 'daphne']) + with mocker.patch.object(reload, '_uwsgi_reload'),\ + mocker.patch.object(reload, '_reset_celery_thread_pool'),\ + mocker.patch.object(reload, '_supervisor_service_restart'): + reload.restart_local_services(['uwsgi', 'celery', 'flower', 'daphne']) + reload._uwsgi_reload.assert_called_once_with() + reload._reset_celery_thread_pool.assert_called_once_with() + reload._supervisor_service_restart.assert_called_once_with(['flower', 'daphne']) + def test_routing_of_service_restarts_diables(mocker): ''' Test that methods are not called if not in the args ''' - with mocker.patch.object(reload, '_uwsgi_reload'): - with mocker.patch.object(reload, '_reset_celery_thread_pool'): - with mocker.patch.object(reload, '_supervisor_service_restart'): - reload.restart_local_services(['flower']) - reload._uwsgi_reload.assert_not_called() - reload._reset_celery_thread_pool.assert_not_called() - reload._supervisor_service_restart.assert_called_once_with(['flower']) + with mocker.patch.object(reload, '_uwsgi_reload'),\ + mocker.patch.object(reload, '_reset_celery_thread_pool'),\ + mocker.patch.object(reload, '_supervisor_service_restart'): + reload.restart_local_services(['flower']) + reload._uwsgi_reload.assert_not_called() + reload._reset_celery_thread_pool.assert_not_called() + reload._supervisor_service_restart.assert_called_once_with(['flower']) From d687ff1be084053f08b5e58a895638bb0925c502 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 15 Feb 2017 15:34:14 -0500 Subject: [PATCH 122/260] delete disable_signals fixture that is not being used --- awx/main/tests/functional/conftest.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 165dfed0d6..3d79ca4c4c 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -56,15 +56,6 @@ def clear_cache(): cache.clear() -@pytest.fixture(scope="session", autouse=False) -def disable_signals(): - ''' - Disable all django model signals. - ''' - mocked = mock.patch('django.dispatch.Signal.send', autospec=True) - mocked.start() - - @pytest.fixture(scope="session", autouse=True) def celery_memory_broker(): ''' From 6e9488a59bad5b97acb1c80c56c1ca8a907f33b4 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 14 Feb 2017 14:36:09 -0500 Subject: [PATCH 123/260] ensure job deps are created only once --- awx/main/scheduler/__init__.py | 23 ++++++++++++++++++++++- awx/main/scheduler/dependency_graph.py | 16 +++++----------- awx/main/tests/unit/scheduler/conftest.py | 3 ++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/awx/main/scheduler/__init__.py b/awx/main/scheduler/__init__.py index 50a80b7116..8d29f0010c 100644 --- a/awx/main/scheduler/__init__.py +++ b/awx/main/scheduler/__init__.py @@ -251,6 +251,18 @@ class TaskManager(): dep.save() inventory_task = InventoryUpdateDict.get_partial(dep.id) + + ''' + Update internal datastructures with the newly created inventory update + ''' + # Should be only 1 inventory update. The one for the job (task) + latest_inventory_updates = self.get_latest_inventory_update_tasks([task]) + self.process_latest_inventory_updates(latest_inventory_updates) + + inventory_sources = self.get_inventory_source_tasks([task]) + self.process_inventory_sources(inventory_sources) + + self.graph.add_job(inventory_task) return inventory_task @@ -271,9 +283,15 @@ class TaskManager(): def capture_chain_failure_dependencies(self, task, dependencies): for dep in dependencies: - dep_obj = task.get_full() + dep_obj = dep.get_full() dep_obj.dependent_jobs.add(task['id']) dep_obj.save() + ''' + if not 'dependent_jobs__id' in task.data: + task.data['dependent_jobs__id'] = [dep_obj.data['id']] + else: + task.data['dependent_jobs__id'].append(dep_obj.data['id']) + ''' def generate_dependencies(self, task): dependencies = [] @@ -291,6 +309,9 @@ class TaskManager(): ''' inventory_sources_already_updated = task.get_inventory_sources_already_updated() + ''' + get_inventory_sources() only return update on launch sources + ''' for inventory_source_task in self.graph.get_inventory_sources(task['inventory_id']): if inventory_source_task['id'] in inventory_sources_already_updated: continue diff --git a/awx/main/scheduler/dependency_graph.py b/awx/main/scheduler/dependency_graph.py index 846a194b27..61f08c4241 100644 --- a/awx/main/scheduler/dependency_graph.py +++ b/awx/main/scheduler/dependency_graph.py @@ -113,21 +113,15 @@ class DependencyGraph(object): def should_update_related_inventory_source(self, job, inventory_source_id): now = self.get_now() + + # Already processed dependencies for this job + if job.data['dependent_jobs__id'] is not None: + return False + latest_inventory_update = self.data[self.LATEST_INVENTORY_UPDATES].get(inventory_source_id, None) if not latest_inventory_update: return True - ''' - This is a bit of fuzzy logic. - If the latest inventory update has a created time == job_created_time-2 - then consider the inventory update found. This is so we don't enter an infinite loop - of updating the project when cache timeout is 0. - ''' - if latest_inventory_update['inventory_source__update_cache_timeout'] == 0 and \ - latest_inventory_update['launch_type'] == 'dependency' and \ - latest_inventory_update['created'] == job['created'] - timedelta(seconds=2): - return False - ''' Normal, expected, cache timeout logic ''' diff --git a/awx/main/tests/unit/scheduler/conftest.py b/awx/main/tests/unit/scheduler/conftest.py index 40e221d0cc..8f3c5f913e 100644 --- a/awx/main/tests/unit/scheduler/conftest.py +++ b/awx/main/tests/unit/scheduler/conftest.py @@ -223,7 +223,8 @@ def job_factory(epoch): 'celery_task_id': '', 'project__scm_update_on_launch': project__scm_update_on_launch, 'inventory__inventory_sources': inventory__inventory_sources, - 'forks': 5 + 'forks': 5, + 'dependent_jobs__id': None, }) return fn From 689dd45c28b00f617685cf74e21cde62565728bd Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 15 Feb 2017 16:26:08 -0500 Subject: [PATCH 124/260] don't trim newlines from custom inventory scripts; they may be relevant see: #5387 angular defaults ngTrim to `true` for