From ee3c838d74c88f99a6fac5cab152a7686d5ccbfe Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 21 Nov 2014 13:44:03 -0500 Subject: [PATCH 01/12] Sources Added some changes to fields in the source form. --- awx/ui/static/js/forms/Source.js | 54 +++++++++++++-------- awx/ui/static/js/helpers/CustomInventory.js | 7 +-- awx/ui/static/js/helpers/Groups.js | 42 +++++++++++++--- awx/ui/static/js/lists/CustomInventory.js | 2 +- awx/ui/static/partials/configure_tower.html | 3 +- awx/ui/templates/ui/index.html | 32 +++++++++++- 6 files changed, 106 insertions(+), 34 deletions(-) diff --git a/awx/ui/static/js/forms/Source.js b/awx/ui/static/js/forms/Source.js index 0e35a89e55..6973716021 100644 --- a/awx/ui/static/js/forms/Source.js +++ b/awx/ui/static/js/forms/Source.js @@ -75,7 +75,7 @@ angular.module('SourceFormDefinition', []) dataContainer: 'body' }, group_by: { - label: 'Group By', + label: 'Only Group By', type: 'text', ngShow: "source && source.value == 'ec2'", addRequired: false, @@ -83,40 +83,54 @@ angular.module('SourceFormDefinition', []) awMultiselect: 'group_by_choices', dataTitle: 'Group By', dataPlacement: 'right', - awPopOver: "

FIXME: Create these automatic groups by default.

", + awPopOver: "

FIXME: Create these automatic groups by default. give examples

", dataContainer: 'body' }, - group_tag_filters: { - label: 'Tag Filters', - type: 'text', - ngShow: "source && source.value == 'ec2' && group_by.value.indexOf('tag_keys') >= 0", // FIXME: Not sure what's needed to make the last expression work. - addRequired: false, - editRequired: false, - dataTitle: 'Tag Filters', - dataPlacement: 'right', - awPopOver: "

FIXME: When grouping by tags, specify which tag keys become groups.

", - dataContainer: 'body' - }, - custom_script: { + // group_tag_filters: { + // label: 'Tag Filters', + // type: 'text', + // ngShow: "source && source.value == 'ec2' && group_by.value.indexOf('tag_keys') >= 0", // FIXME: Not sure what's needed to make the last expression work. + // addRequired: false, + // editRequired: false, + // dataTitle: 'Tag Filters', + // dataPlacement: 'right', + // awPopOver: "

FIXME: When grouping by tags, specify which tag keys become groups.

", + // dataContainer: 'body' + // }, + source_script: { label : "Custom Inventory Scripts", type: 'lookup', ngShow: "source && source.value !== '' && source.value === 'custom'", - sourceModel: 'custom_script', + sourceModel: 'source_script', sourceField: 'name', - ngClick: 'lookUpCustomScript()', + ngClick: 'lookUpCustom_inventory()', addRequired: false, editRequired: false }, - source_vars: { - label: 'Source Variables', - ngShow: "source && (source.value == 'file' || source.value == 'ec2' || source.value == 'custom')", + extra_vars: { + label: 'Environment Variables', //"{{vars_label}}" , + ngShow: "source && (source.value=='custom')", type: 'textarea', addRequired: false, editRequird: false, rows: 6, 'default': '---', parseTypeName: 'envParseType', - dataTitle: 'Source Variables', + dataTitle: "Environment Variables", //'

Source Variables

', + dataPlacement: 'right', + awPopOver: "

These are environment variables

", + dataContainer: 'body' + }, + source_vars: { + label: 'Source Variables', //"{{vars_label}}" , + ngShow: "source && (source.value == 'file' || source.value == 'ec2')", + type: 'textarea', + addRequired: false, + editRequird: false, + rows: 6, + 'default': '---', + parseTypeName: 'envParseType', + dataTitle: "Source Variables", dataPlacement: 'right', awPopOver: "

Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables " + "" + diff --git a/awx/ui/static/js/helpers/CustomInventory.js b/awx/ui/static/js/helpers/CustomInventory.js index 64e61952d7..93cd236e2b 100644 --- a/awx/ui/static/js/helpers/CustomInventory.js +++ b/awx/ui/static/js/helpers/CustomInventory.js @@ -63,7 +63,7 @@ angular.module('CreateCustomInventoryHelper', [ 'Utilities', 'RestServices', 'Sc SearchInit({ scope: scope, - set: 'custum_inventories', + set: 'custom_inventories', list: list, url: defaultUrl }); @@ -187,12 +187,10 @@ function($compile, SchedulerInit, Rest, Wait, CustomInventoryList, CustomInvento view = GenerateList, list = CustomInventoryList, url = GetBasePath('inventory_scripts'); - // base = $location.path().replace(/^\//, '').split('/')[0]; generator.inject(form, { id:'custom-script-dialog', mode: 'add' , scope:scope, related: false, breadCrumbs: false}); generator.reset(); - // Save scope.formSave = function () { generator.clearApiErrors(); @@ -211,7 +209,7 @@ function($compile, SchedulerInit, Rest, Wait, CustomInventoryList, CustomInvento SearchInit({ scope: scope, - set: 'custum_inventories', + set: 'custom_inventories', list: list, url: url }); @@ -252,7 +250,6 @@ function($compile, CustomInventoryList, Rest, Wait, GenerateList, CustomInventor list = CustomInventoryList, master = {}, url = GetBasePath('inventory_scripts'); - // base = $location.path().replace(/^\//, '').split('/')[0]; generator.inject(form, { id:'custom-script-dialog', diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 58051cde40..091b12e00c 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -207,13 +207,13 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' } ]) -.factory('SourceChange', ['GetBasePath', 'CredentialList', 'LookUpInit', 'Empty', 'Wait', 'ParseTypeChange', - function (GetBasePath, CredentialList, LookUpInit, Empty, Wait, ParseTypeChange) { +.factory('SourceChange', ['GetBasePath', 'CredentialList', 'LookUpInit', 'Empty', 'Wait', 'ParseTypeChange', 'CustomInventoryList' , + function (GetBasePath, CredentialList, LookUpInit, Empty, Wait, ParseTypeChange, CustomInventoryList) { return function (params) { var scope = params.scope, form = params.form, - kind, url, callback; + kind, url, callback, invUrl; if (!Empty(scope.source)) { if (scope.source.value === 'file') { @@ -234,7 +234,6 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' $('#source_form').removeClass('squeeze'); } else if (scope.source.value === 'ec2') { scope.source_region_choices = scope.ec2_regions; - //$('#s2id_group_source_regions').select2('data', []); $('#s2id_source_source_regions').select2('data', [{ id: 'all', text: 'All' @@ -273,6 +272,21 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' }]); $('#source_form').addClass('squeeze'); } + if(scope.source.value==="custom"){ + invUrl = GetBasePath('inventory_scripts'); + LookUpInit({ + url: invUrl, + scope: scope, + form: form, + // current_item: null, + list: CustomInventoryList, + field: 'source_script', + input_type: 'radio' + }); + scope.extra_vars = (Empty(scope.source_vars)) ? "---" : scope.source_vars; + ParseTypeChange({ scope: scope, variable: 'extra_vars', parse_variable: form.fields.extra_vars.parseTypeName, + field_id: 'source_extra_vars', onReady: callback }); + } if (scope.source.value === 'rax' || scope.source.value === 'ec2'|| scope.source.value==='gce' || scope.source.value === 'azure' || scope.source.value === 'vmware') { kind = (scope.source.value === 'rax') ? 'rax' : (scope.source.value==='gce') ? 'gce' : (scope.source.value==='azure') ? 'azure' : (scope.source.value === 'vmware') ? 'vmware' : 'aws' ; url = GetBasePath('credentials') + '?cloud=true&kind=' + kind; @@ -850,6 +864,8 @@ function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, Sched Wait('start'); ParseTypeChange({ scope: sources_scope, variable: 'source_vars', parse_variable: SourceForm.fields.source_vars.parseTypeName, field_id: 'source_source_vars', onReady: waitStop }); + ParseTypeChange({ scope: sources_scope, variable: 'extra_vars', parse_variable: SourceForm.fields.extra_vars.parseTypeName, + field_id: 'source_extra_vars', onReady: waitStop }); } } else if ($(e.target).text() === 'Schedule') { @@ -946,7 +962,12 @@ function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, Sched // Parse source_vars, converting to YAML. sources_scope.source_vars = ParseVariableString(data.source_vars); master.source_vars = sources_scope.variables; - } else if (data[fld] !== undefined) { + } + // else if(fld === "source_script"){ + // sources_scope[fld] = data + // } + + else if (data[fld] !== undefined) { sources_scope[fld] = data[fld]; master[fld] = sources_scope[fld]; } @@ -1144,6 +1165,7 @@ function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, Sched credential: sources_scope.credential, overwrite: sources_scope.overwrite, overwrite_vars: sources_scope.overwrite_vars, + source_script: sources_scope.source_script, update_on_launch: sources_scope.update_on_launch, update_cache_timeout: (sources_scope.update_cache_timeout || 0) }; @@ -1156,11 +1178,19 @@ function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, Sched } data.source_regions = r.join(); - if (sources_scope.source && (sources_scope.source.value === 'ec2' || sources_scope.source.value === 'custom')) { + if (sources_scope.source && (sources_scope.source.value === 'ec2')) { // for ec2, validate variable data data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.source_vars, true); } + if (sources_scope.source && (sources_scope.source.value === 'custom')) { + data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.extra_vars, true); + } + + if(sources_scope.source.value === 'custom'){ + delete(data.credential); + } + if (!parseError) { Rest.setUrl(sources_scope.source_url); Rest.put(data) diff --git a/awx/ui/static/js/lists/CustomInventory.js b/awx/ui/static/js/lists/CustomInventory.js index 037ae99dfc..b1be6c4ea2 100644 --- a/awx/ui/static/js/lists/CustomInventory.js +++ b/awx/ui/static/js/lists/CustomInventory.js @@ -12,7 +12,7 @@ angular.module('CustomInventoryListDefinition', []) .value('CustomInventoryList', { - name: 'custum_inventories', + name: 'custom_inventories', iterator: 'custom_inventory', selectTitle: 'Add custom inventory', editTitle: 'Custom Inventories', diff --git a/awx/ui/static/partials/configure_tower.html b/awx/ui/static/partials/configure_tower.html index 7b4ca42c0c..ea36a9b539 100644 --- a/awx/ui/static/partials/configure_tower.html +++ b/awx/ui/static/partials/configure_tower.html @@ -1,4 +1,5 @@ -

+ +
diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index d2adf13967..b695b1dce3 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -398,7 +398,37 @@ - +
+ From 226c31bc35728ba7bf1dcabf5ec09818bc691326 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 21 Nov 2014 14:01:30 -0500 Subject: [PATCH 02/12] Source.js Changed the tooltip for environment variables for custom inventory script --- awx/ui/static/js/forms/Source.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/ui/static/js/forms/Source.js b/awx/ui/static/js/forms/Source.js index 6973716021..e65ad00dff 100644 --- a/awx/ui/static/js/forms/Source.js +++ b/awx/ui/static/js/forms/Source.js @@ -118,7 +118,11 @@ angular.module('SourceFormDefinition', []) parseTypeName: 'envParseType', dataTitle: "Environment Variables", //'

Source Variables

', dataPlacement: 'right', - awPopOver: "

These are environment variables

", + awPopOver: "

Provide key/value pairs using either YAML or JSON.

" + + "JSON:
\n" + + "
{
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n", dataContainer: 'body' }, source_vars: { From 2916ebf0c078ee74cb7cf7d21a31dc2e4bf3767e Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 21 Nov 2014 15:57:01 -0500 Subject: [PATCH 03/12] Improve query time by removing a check from the order by filter handler --- awx/api/filters.py | 6 ------ awx/api/views.py | 3 +++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/awx/api/filters.py b/awx/api/filters.py index bc25acb4ed..bf1f356e68 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -277,12 +277,6 @@ class OrderByBackend(BaseFilterBackend): if field not in ('type', '-type'): new_order_by.append(field) queryset = queryset.order_by(*new_order_by) - # Fetch the first result to run the query, otherwise we don't - # always catch the FieldError for invalid field names. - try: - queryset[0] - except IndexError: - pass return queryset except FieldError, e: # Return a 400 for invalid field names. diff --git a/awx/api/views.py b/awx/api/views.py index 953708fa90..b7c39ae0ed 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -15,6 +15,7 @@ import sys from django.conf import settings from django.contrib.auth.models import User from django.core.urlresolvers import reverse +from django.core.exceptions import FieldError from django.db.models import Q, Count, Sum from django.db import IntegrityError, transaction from django.shortcuts import get_object_or_404 @@ -62,6 +63,8 @@ def api_exception_handler(exc): ''' if isinstance(exc, IntegrityError): exc = ParseError(exc.args[0]) + if isinstance(exc, FieldError): + exc = ParseError(exc.args[0]) return exception_handler(exc) From 300fb677fac8fbe80cfbe387ffce62a8e3a22061 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 24 Nov 2014 13:29:30 -0500 Subject: [PATCH 04/12] Portal mode pagination fixed the pagination on the job templates portal widget. To fix this i had to include an '&' in the pagination url creation in paginationhelpers.js. not sure if this could have effect on anything else that uses pagination. --- awx/ui/static/js/controllers/Portal.js | 63 +++++++++++++------ awx/ui/static/js/helpers/PaginationHelpers.js | 2 +- awx/ui/static/js/widgets/PortalJobs.js | 10 +-- awx/ui/templates/ui/index.html | 2 +- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/awx/ui/static/js/controllers/Portal.js b/awx/ui/static/js/controllers/Portal.js index 95bdc98b14..035c62fb97 100644 --- a/awx/ui/static/js/controllers/Portal.js +++ b/awx/ui/static/js/controllers/Portal.js @@ -35,6 +35,7 @@ function PortalController($scope, $compile, $routeParams, $rootScope, $location, list = PortalJobTemplateList, view= GenerateList, defaultUrl = GetBasePath('job_templates'), + max_rows, buttons = { refresh: { mode: 'all', @@ -79,6 +80,7 @@ function PortalController($scope, $compile, $routeParams, $rootScope, $location, searchSize: 'col-lg-6 col-md-6' }); + $scope.job_templatePageSize = $scope.getMaxRows(); SearchInit({ scope: $scope, @@ -89,16 +91,10 @@ function PortalController($scope, $compile, $routeParams, $rootScope, $location, PaginateInit({ scope: $scope, list: list, - url: defaultUrl + url: defaultUrl, + pageSize: $scope.job_templatePageSize }); - // Called from Inventories tab, host failed events link: - if ($routeParams.name) { - $scope[list.iterator + 'SearchField'] = 'name'; - $scope[list.iterator + 'SearchValue'] = $routeParams.name; - $scope[list.iterator + 'SearchFieldLabel'] = list.fields.name.label; - } - $scope.search(list.iterator); PortalJobsWidget({ @@ -126,22 +122,53 @@ function PortalController($scope, $compile, $routeParams, $rootScope, $location, jobs_scope.search('portal_job'); //processEvent(event); }); + $scope.getMaxRows = function(){ + var docw = $(window).width(), + box_height, available_height, search_row, page_row, height, header, row_height; + + available_height = Math.floor($(window).height() - $('#main-menu-container .navbar').outerHeight() - $('#refresh-row').outerHeight() - 35); + $('.portal-job-template-container').height(available_height); + $('.portal-container').height(available_height); + search_row = Math.max($('.search-row:eq(0)').outerHeight(), 50); + page_row = Math.max($('.page-row:eq(0)').outerHeight(), 33); + header = 0; //Math.max($('#completed_jobs_table thead').height(), 41); + height = Math.floor(available_height) - header - page_row - search_row ; + if (docw < 765 && docw >= 493) { + row_height = 27; + } + else if (docw < 493) { + row_height = 47; + } + else if (docw < 865) { + row_height = 87; + } + else if (docw < 925) { + row_height = 67; + } + else if (docw < 1415) { + row_height = 47; + } + else { + row_height = 35; + } + max_rows = Math.floor(height / row_height); + if (max_rows < 5){ + box_height = header+page_row + search_row + 40 + (5 * row_height); + if (docw < 1140) { + box_height += 40; + } + // $('.portal-job-template-container').height(box_height); + max_rows = 5; + } + return max_rows; + }; + $scope.submitJob = function (id) { PlaybookRun({ scope: $scope, id: id }); }; $scope.refresh = function () { $scope.$emit('LoadPortal'); - // Wait('start'); - // loadedCount = 0; - // Rest.setUrl(GetBasePath('dashboard')); - // Rest.get() - // .success(function (data) { - // $scope.$emit('dashboardReady', data); - // }) - // .error(function (data, status) { - // ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard: ' + status }); - // }); }; $scope.refresh(); diff --git a/awx/ui/static/js/helpers/PaginationHelpers.js b/awx/ui/static/js/helpers/PaginationHelpers.js index f1d25531a2..87ac589aad 100644 --- a/awx/ui/static/js/helpers/PaginationHelpers.js +++ b/awx/ui/static/js/helpers/PaginationHelpers.js @@ -138,7 +138,7 @@ angular.module('PaginationHelpers', ['Utilities', 'RefreshHelper', 'RefreshRelat connect = (/\/$/.test(new_url)) ? '?' : '&'; new_url += connect + 'page=' + page; new_url += (scope[iterator + 'SearchParams']) ? '&' + scope[iterator + 'SearchParams'] + - '&page_size=' + scope[iterator + '_page_size'] : 'page_size=' + scope[iterator + 'PageSize']; + '&page_size=' + scope[iterator + '_page_size'] : '&page_size=' + scope[iterator + 'PageSize']; Wait('start'); Refresh({ scope: scope, set: set, iterator: iterator, url: new_url }); }; diff --git a/awx/ui/static/js/widgets/PortalJobs.js b/awx/ui/static/js/widgets/PortalJobs.js index 84d17a51d9..dddfde7a45 100644 --- a/awx/ui/static/js/widgets/PortalJobs.js +++ b/awx/ui/static/js/widgets/PortalJobs.js @@ -11,8 +11,8 @@ 'use strict'; angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) -.factory('PortalJobsWidget', ['$rootScope', '$compile', 'LoadSchedulesScope', 'LoadJobsScope', 'PortalJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', - function ($rootScope, $compile, LoadSchedulesScope, LoadJobsScope, PortalJobsList, ScheduledJobsList, GetChoices, GetBasePath) { +.factory('PortalJobsWidget', ['$rootScope', '$compile', 'LoadSchedulesScope', 'LoadJobsScope', 'PortalJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'PortalJobTemplateList', + function ($rootScope, $compile, LoadSchedulesScope, LoadJobsScope, PortalJobsList, ScheduledJobsList, GetChoices, GetBasePath, PortalJobTemplateList) { return function (params) { var scope = params.scope, target = params.target, @@ -70,6 +70,7 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) }); + $(window).resize(_.debounce(function() { resizePortalJobsWidget(); }, 500)); @@ -150,8 +151,9 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) setPortalJobsHeight(); jobs_scope[PortalJobsList.iterator + '_page_size'] = max_rows; jobs_scope.changePageSize(PortalJobsList.name, PortalJobsList.iterator, false); - // scheduled_scope[ScheduledJobsList.iterator + '_page_size'] = max_rows; - // scheduled_scope.changePageSize(ScheduledJobsList.name, ScheduledJobsList.iterator, false); + scope[PortalJobTemplateList.iterator + '_page_size'] = max_rows; + scope[PortalJobTemplateList.iterator + 'PageSize'] = max_rows; + scope.changePageSize(PortalJobTemplateList.name, PortalJobTemplateList.iterator, false); } diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index b695b1dce3..1f8604ffdb 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -205,7 +205,7 @@ Monitor Tower Portal Mode View License - Exit Portal Mode + Exit Portal Logout From cfb4361eb40fabc5535d7d9655d92a0ad7790cd7 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 24 Nov 2014 15:24:11 -0500 Subject: [PATCH 05/12] Temporarily remove ask_variables for job relaunch --- 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 b7c39ae0ed..196a2807ba 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1882,7 +1882,7 @@ class JobRelaunch(GenericAPIView): obj = self.get_object() data = {} data['passwords_needed_to_start'] = obj.passwords_needed_to_start - data['ask_variables_on_launch'] = obj.ask_variables_on_launch + #data['ask_variables_on_launch'] = obj.ask_variables_on_launch return Response(data) def post(self, request, *args, **kwargs): From f49fe9ff56fe533d6640484f764f0483273c19fa Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Mon, 24 Nov 2014 14:41:06 -0600 Subject: [PATCH 06/12] Make it faster. --- awx/main/access.py | 100 ++++++++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 547407b8fb..a9f781b5a5 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -9,6 +9,7 @@ import logging from django.conf import settings from django.db.models import F, Q from django.contrib.auth.models import User +from django.utils.functional import cached_property # Django REST Framework from rest_framework.exceptions import ParseError, PermissionDenied @@ -531,19 +532,31 @@ class CredentialAccess(BaseAccess): model = Credential def get_queryset(self): + """Return the queryset for credentials, based on what the user is + permitted to see. + """ + # Create a base queryset. + # If the user is a superuser, and therefore can see everything, this + # is also sufficient, and we are done. qs = self.model.objects.filter(active=True).distinct() qs = qs.select_related('created_by', 'user', 'team') if self.user.is_superuser: return qs - orgs_as_admin = self.user.admin_of_organizations.filter(active=True) + + # Get the list of organizations for which the user is an admin return qs.filter( Q(user=self.user) | - Q(user__organizations__in=orgs_as_admin) | - Q(user__admin_of_organizations__in=orgs_as_admin) | - Q(team__organization__in=orgs_as_admin, team__active=True) | + Q(user__organizations__id__in=self._orgs_as_admin) | + Q(user__admin_of_organizations__id__in=self._orgs_as_admin) | + Q(team__organization__id__in=self._orgs_as_admin, team__active=True) | Q(team__users__in=[self.user], team__active=True) ) + @cached_property + def _orgs_as_admin(self): + orgs = self.user.admin_of_organizations.filter(active=True).values('id') + return [i['id'] for i in orgs] + def can_add(self, data): if self.user.is_superuser: return True @@ -817,10 +830,11 @@ class JobTemplateAccess(BaseAccess): 'credential') if self.user.is_superuser: return qs - credential_qs = self.user.get_queryset(Credential) + + credential_ids = [x.id for x in self.user.get_queryset(Credential)] base_qs = qs.filter( - Q(credential__in=credential_qs) | Q(credential__isnull=True), - Q(cloud_credential__in=credential_qs) | Q(cloud_credential__isnull=True), + Q(credential__in=credential_ids) | Q(credential__isnull=True), + Q(cloud_credential__in=credential_ids) | Q(cloud_credential__isnull=True), ) # FIXME: Check active status on related objects! org_admin_qs = base_qs.filter( @@ -830,35 +844,30 @@ class JobTemplateAccess(BaseAccess): allowed_deploy = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY] allowed_check = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK] - # perm_qs = base_qs.filter( - # Q(inventory__permissions__user=self.user) | Q(inventory__permissions__team__users__in=[self.user]), - # Q(project__permissions__user=self.user) | Q(project__permissions__team__users__in=[self.user]), - # inventory__permissions__permission_type__in=allowed, - # project__permissions__permission_type__in=allowed, - # inventory__permissions__active=True, - # project__permissions__active=True, - # inventory__permissions__pk=F('project__permissions__pk'), - # ) + team_ids = [i.id for i in Team.objects.filter(users__in=[self.user])] + + deploy_permissions_ids = [i.id for i in Permission.objects.filter( + Q(user=self.user) | Q(team__in=team_ids), + active=True, + permission_type__in=allowed_deploy, + )] + check_permissions_ids = [i.id for i in Permission.objects.filter( + Q(user=self.user) | Q(team__in=team_ids), + active=True, + permission_type__in=allowed_check, + )] perm_deploy_qs = base_qs.filter( - Q(inventory__permissions__user=self.user) | Q(inventory__permissions__team__users__in=[self.user]), - Q(project__permissions__user=self.user) | Q(project__permissions__team__users__in=[self.user]), job_type=PERM_INVENTORY_DEPLOY, - inventory__permissions__permission_type__in=allowed_deploy, - project__permissions__permission_type__in=allowed_deploy, - inventory__permissions__active=True, - project__permissions__active=True, + inventory__permissions__in=deploy_permissions_ids, + project__permissions__in=deploy_permissions_ids, inventory__permissions__pk=F('project__permissions__pk'), ) perm_check_qs = base_qs.filter( - Q(inventory__permissions__user=self.user) | Q(inventory__permissions__team__users__in=[self.user]), - Q(project__permissions__user=self.user) | Q(project__permissions__team__users__in=[self.user]), job_type=PERM_INVENTORY_CHECK, - inventory__permissions__permission_type__in=allowed_check, - project__permissions__permission_type__in=allowed_check, - inventory__permissions__active=True, - project__permissions__active=True, + inventory__permissions__in=check_permissions_ids, + project__permissions__in=check_permissions_ids, inventory__permissions__pk=F('project__permissions__pk'), ) @@ -1014,9 +1023,9 @@ class JobAccess(BaseAccess): 'project', 'credential') if self.user.is_superuser: return qs - credential_qs = self.user.get_queryset(Credential) + credential_ids = [x.id for x in self.user.get_queryset(Credential)] base_qs = qs.filter( - credential__in=credential_qs, + credential__in=credential_ids, ) org_admin_qs = base_qs.filter( project__organizations__admins__in=[self.user] @@ -1036,28 +1045,33 @@ class JobAccess(BaseAccess): # inventory__permissions__pk=F('project__permissions__pk'), # ) + team_ids = [i.id for i in Team.objects.filter(users__in=[self.user])] + + deploy_permissions_ids = [i.id for i in Permission.objects.filter( + Q(user=self.user) | Q(team__in=team_ids), + active=True, + permission_type__in=allowed_deploy, + )] + check_permissions_ids = [i.id for i in Permission.objects.filter( + Q(user=self.user) | Q(team__in=team_ids), + active=True, + permission_type__in=allowed_check, + )] + perm_deploy_qs = base_qs.filter( - Q(inventory__permissions__user=self.user) | Q(inventory__permissions__team__users__in=[self.user]), - Q(project__permissions__user=self.user) | Q(project__permissions__team__users__in=[self.user]), job_type=PERM_INVENTORY_DEPLOY, - inventory__permissions__permission_type__in=allowed_deploy, - project__permissions__permission_type__in=allowed_deploy, - inventory__permissions__active=True, - project__permissions__active=True, + inventory__permissions__in=deploy_permissions_ids, + project__permissions__in=deploy_permissions_ids, inventory__permissions__pk=F('project__permissions__pk'), ) perm_check_qs = base_qs.filter( - Q(inventory__permissions__user=self.user) | Q(inventory__permissions__team__users__in=[self.user]), - Q(project__permissions__user=self.user) | Q(project__permissions__team__users__in=[self.user]), job_type=PERM_INVENTORY_CHECK, - inventory__permissions__permission_type__in=allowed_check, - project__permissions__permission_type__in=allowed_check, - inventory__permissions__active=True, - project__permissions__active=True, + inventory__permissions__in=check_permissions_ids, + project__permissions__in=check_permissions_ids, inventory__permissions__pk=F('project__permissions__pk'), ) - + # FIXME: I *think* this should work... needs more testing. return org_admin_qs | perm_deploy_qs | perm_check_qs From c4bdf0d936f0784eb76309923816a06288fc6262 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 24 Nov 2014 15:53:11 -0500 Subject: [PATCH 07/12] Job submission Fixed some new errors that emerged as a result of adding the prompt for credentials back in and fixing the html generation of code mirror editor for job variables. --- awx/ui/static/js/helpers/JobSubmission.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index a4bea23da5..447f33d845 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -43,8 +43,10 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential } } } - - + delete(job_launch_data.extra_vars); + if(!Empty(scope.credential)){ + job_launch_data.credential = scope.credential; + } Rest.setUrl(url); Rest.post(job_launch_data) .success(function(data) { @@ -386,6 +388,7 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi function buildHtml(extra_vars){ html += GenerateForm.buildHTML(JobVarsPromptForm, { mode: 'edit', modal: true, scope: scope }); + html = html.replace("", ""); scope.helpContainer = "
\n" + "" + @@ -739,6 +742,12 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi if(passwords.length>0){ scope.$emit('PromptForPasswords', passwords, html, url); } + else if (scope.ask_variables_on_launch){ + scope.$emit('PromptForVars', html, url); + } + else if (!Empty(scope.survey_enabled) && scope.survey_enabled===true) { + scope.$emit('PromptForSurvey', html, url); + } else scope.$emit('StartPlaybookRun', url); }) .error(function (data, status) { @@ -758,8 +767,8 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi launch_url = url;//data.related.start; scope.passwords_needed_to_start = data.passwords_needed_to_start; scope.prompt_for_vars = data.ask_variables_on_launch; - // scope.extra_vars = data.variables_needed_to_start; scope.survey_enabled = data.survey_enabled; + scope.ask_variables_on_launch = data.ask_variables_on_launch; html = '
'; From 955af6aa85b22187c69873c63360cc7b50b1c358 Mon Sep 17 00:00:00 2001 From: James Laska Date: Mon, 24 Nov 2014 16:28:13 -0500 Subject: [PATCH 08/12] Add support for customizing managed tower services Allows for overriding the services managed by the ansible-tower initscript. --- tools/scripts/ansible-tower | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tools/scripts/ansible-tower b/tools/scripts/ansible-tower index f124ac5009..049cc32bd4 100755 --- a/tools/scripts/ansible-tower +++ b/tools/scripts/ansible-tower @@ -16,15 +16,20 @@ # Description: Ansible Tower provides an easy-to-use UI and dashboard, role-based access control and more for your Ansible initiative ### END INIT INFO -if [ -e /etc/debian_version ] -then - SERVICES=(postgresql redis-server apache2 supervisor) +# Default configured services +if [ -e /etc/debian_version ]; then + SERVICES="postgresql redis-server apache2 supervisor" + TOWER_CONFIG="/etc/default/tower" else - SERVICES=(postgresql redis httpd supervisord) + SERVICES="postgresql redis httpd supervisord" + TOWER_CONFIG="/etc/sysconfig/tower" fi +# Load any configuration +[ -e "${TOWER_CONFIG}" ] && . "${TOWER_CONFIG}" + service_action() { - for svc in ${SERVICES[@]}; do + for svc in ${SERVICES}; do service ${svc} $1 this_return=$? if [ $this_return -gt $worst_return ]; then From ad856391f6022a398045ff0bfd5f285d2564233d Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 24 Nov 2014 21:12:21 -0500 Subject: [PATCH 09/12] Use select_related and other misc fixes to improve performance and reduce queries. --- awx/api/serializers.py | 13 ++- awx/main/access.py | 142 ++++++++++++++++++-------------- awx/main/models/unified_jobs.py | 14 ++++ 3 files changed, 104 insertions(+), 65 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index d86f952549..3369d51dfc 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -266,6 +266,13 @@ class BaseSerializer(serializers.ModelSerializer): summary_fields = SortedDict() for fk, related_fields in SUMMARIZABLE_FK_FIELDS.items(): try: + # A few special cases where we don't want to access the field + # because it results in additional queries. + if fk == 'job' and isinstance(obj, UnifiedJob): + continue + if fk == 'project' and isinstance(obj, InventorySource): + continue + fkval = getattr(obj, fk, None) if fkval is None: continue @@ -777,7 +784,7 @@ class HostSerializer(BaseSerializerWithVariables): else "", 'status': j.job.status, 'finished': j.job.finished, - } for j in obj.job_host_summaries.filter(job__active=True).order_by('-created')[:5]]}) + } for j in obj.job_host_summaries.filter(job__active=True).select_related('job__job_template').order_by('-created')[:5]]}) return d def _get_host_port_from_name(self, name): @@ -1452,9 +1459,7 @@ class ScheduleSerializer(BaseSerializer): unified_jobs = reverse('api:schedule_unified_jobs_list', args=(obj.pk,)), )) if obj.unified_job_template and obj.unified_job_template.active: - #TODO: Figure out why we have to do this - ujt = UnifiedJobTemplate.objects.get(id=obj.unified_job_template.id) - res['unified_job_template'] = ujt.get_absolute_url() #obj.unified_job_template.get_absolute_url() + res['unified_job_template'] = obj.unified_job_template.get_absolute_url() return res def validate_unified_job_template(self, attrs, source): diff --git a/awx/main/access.py b/awx/main/access.py index a9f781b5a5..bae789f871 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -9,7 +9,6 @@ import logging from django.conf import settings from django.db.models import F, Q from django.contrib.auth.models import User -from django.utils.functional import cached_property # Django REST Framework from rest_framework.exceptions import ParseError, PermissionDenied @@ -224,7 +223,7 @@ class OrganizationAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.filter(active=True).distinct() - qs = qs.select_related('created_by') + qs = qs.select_related('created_by', 'modified_by') if self.user.is_superuser: return qs return qs.filter(Q(admins__in=[self.user]) | Q(users__in=[self.user])) @@ -257,7 +256,7 @@ class InventoryAccess(BaseAccess): def get_queryset(self, allowed=None): allowed = allowed or PERMISSION_TYPES_ALLOWING_INVENTORY_READ qs = Inventory.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'organization') + qs = qs.select_related('created_by', 'modified_by', 'organization') if self.user.is_superuser: return qs qs = qs.filter(organization__active=True) @@ -332,12 +331,12 @@ class HostAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'inventory', + qs = qs.select_related('created_by', 'modified_by', 'inventory', 'last_job__job_template', - 'last_job_host_summary') + 'last_job_host_summary__job') qs = qs.prefetch_related('groups') - inventories_qs = self.user.get_queryset(Inventory) - return qs.filter(inventory__in=inventories_qs) + inventory_ids = set(self.user.get_queryset(Inventory).values_list('id', flat=True)) + return qs.filter(inventory_id__in=inventory_ids) def can_read(self, obj): return obj and self.user.can_access(Inventory, 'read', obj.inventory) @@ -405,10 +404,10 @@ class GroupAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'inventory') + qs = qs.select_related('created_by', 'modified_by', 'inventory') qs = qs.prefetch_related('parents', 'children', 'inventory_source') - inventories_qs = self.user.get_queryset(Inventory) - return qs.filter(inventory__in=inventories_qs) + inventory_ids = set(self.user.get_queryset(Inventory).values_list('id', flat=True)) + return qs.filter(inventory_id__in=inventory_ids) def can_read(self, obj): return obj and self.user.can_access(Inventory, 'read', obj.inventory) @@ -465,10 +464,10 @@ class InventorySourceAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'group', 'inventory') - inventories_qs = self.user.get_queryset(Inventory) - return qs.filter(Q(inventory__in=inventories_qs) | - Q(group__inventory__in=inventories_qs)) + qs = qs.select_related('created_by', 'modified_by', 'group', 'inventory') + inventory_ids = set(self.user.get_queryset(Inventory).values_list('id', flat=True)) + return qs.filter(Q(inventory_id__in=inventory_ids) | + Q(group__inventory_id__in=inventory_ids)) def can_read(self, obj): if obj and obj.group: @@ -505,7 +504,7 @@ class InventoryUpdateAccess(BaseAccess): def get_queryset(self): qs = InventoryUpdate.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'inventory_source__group', + qs = qs.select_related('created_by', 'modified_by', 'inventory_source__group', 'inventory_source__inventory') inventory_sources_qs = self.user.get_queryset(InventorySource) return qs.filter(inventory_source__in=inventory_sources_qs) @@ -539,24 +538,20 @@ class CredentialAccess(BaseAccess): # If the user is a superuser, and therefore can see everything, this # is also sufficient, and we are done. qs = self.model.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'user', 'team') + qs = qs.select_related('created_by', 'modified_by', 'user', 'team') if self.user.is_superuser: return qs # Get the list of organizations for which the user is an admin + orgs_as_admin_ids = set(self.user.admin_of_organizations.filter(active=True).values_list('id', flat=True)) return qs.filter( Q(user=self.user) | - Q(user__organizations__id__in=self._orgs_as_admin) | - Q(user__admin_of_organizations__id__in=self._orgs_as_admin) | - Q(team__organization__id__in=self._orgs_as_admin, team__active=True) | + Q(user__organizations__id__in=orgs_as_admin_ids) | + Q(user__admin_of_organizations__id__in=orgs_as_admin_ids) | + Q(team__organization__id__in=orgs_as_admin_ids, team__active=True) | Q(team__users__in=[self.user], team__active=True) ) - @cached_property - def _orgs_as_admin(self): - orgs = self.user.admin_of_organizations.filter(active=True).values('id') - return [i['id'] for i in orgs] - def can_add(self, data): if self.user.is_superuser: return True @@ -611,7 +606,7 @@ class TeamAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'organization') + qs = qs.select_related('created_by', 'modified_by', 'organization') if self.user.is_superuser: return qs return qs.filter( @@ -663,7 +658,7 @@ class ProjectAccess(BaseAccess): def get_queryset(self): qs = Project.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'current_update', 'last_update') + qs = qs.select_related('created_by', 'modified_by', 'credential', 'current_update', 'last_update') if self.user.is_superuser: return qs allowed = [PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK] @@ -709,9 +704,9 @@ class ProjectUpdateAccess(BaseAccess): def get_queryset(self): qs = ProjectUpdate.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'project') - projects_qs = self.user.get_queryset(Project) - return qs.filter(project__in=projects_qs) + qs = qs.select_related('created_by', 'modified_by', 'project') + project_ids = set(self.user.get_queryset(Project).values_list('id', flat=True)) + return qs.filter(project_id__in=project_ids) def can_cancel(self, obj): return self.can_change(obj, {}) and obj.can_cancel @@ -734,15 +729,15 @@ class PermissionAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'user', 'team', 'inventory', + qs = qs.select_related('created_by', 'modified_by', 'user', 'team', 'inventory', 'project') if self.user.is_superuser: return qs - orgs_as_admin = self.user.admin_of_organizations.filter(active=True) + orgs_as_admin_ids = set(self.user.admin_of_organizations.filter(active=True).values_list('id', flat=True)) return qs.filter( - Q(user__organizations__in=orgs_as_admin) | - Q(user__admin_of_organizations__in=orgs_as_admin) | - Q(team__organization__in=orgs_as_admin, team__active=True) | + Q(user__organizations__in=orgs_as_admin_ids) | + Q(user__admin_of_organizations__in=orgs_as_admin_ids) | + Q(team__organization__in=orgs_as_admin_ids, team__active=True) | Q(user=self.user) | Q(team__users__in=[self.user], team__active=True) ) @@ -826,15 +821,14 @@ class JobTemplateAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'inventory', 'project', - 'credential') + qs = qs.select_related('created_by', 'modified_by', 'inventory', 'project', + 'credential', 'cloud_credential', 'next_schedule') if self.user.is_superuser: return qs - - credential_ids = [x.id for x in self.user.get_queryset(Credential)] + credential_ids = set(self.user.get_queryset(Credential).values_list('id', flat=True)) base_qs = qs.filter( - Q(credential__in=credential_ids) | Q(credential__isnull=True), - Q(cloud_credential__in=credential_ids) | Q(cloud_credential__isnull=True), + Q(credential_id__in=credential_ids) | Q(credential__isnull=True), + Q(cloud_credential_id__in=credential_ids) | Q(cloud_credential__isnull=True), ) # FIXME: Check active status on related objects! org_admin_qs = base_qs.filter( @@ -844,18 +838,18 @@ class JobTemplateAccess(BaseAccess): allowed_deploy = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY] allowed_check = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK] - team_ids = [i.id for i in Team.objects.filter(users__in=[self.user])] + team_ids = set(Team.objects.filter(users__in=[self.user]).values_list('id', flat=True)) - deploy_permissions_ids = [i.id for i in Permission.objects.filter( - Q(user=self.user) | Q(team__in=team_ids), + deploy_permissions_ids = set(Permission.objects.filter( + Q(user=self.user) | Q(team_id__in=team_ids), active=True, permission_type__in=allowed_deploy, - )] - check_permissions_ids = [i.id for i in Permission.objects.filter( - Q(user=self.user) | Q(team__in=team_ids), + ).values_list('id', flat=True)) + check_permissions_ids = set(Permission.objects.filter( + Q(user=self.user) | Q(team_id__in=team_ids), active=True, permission_type__in=allowed_check, - )] + ).values_list('id', flat=True)) perm_deploy_qs = base_qs.filter( job_type=PERM_INVENTORY_DEPLOY, @@ -1019,13 +1013,14 @@ class JobAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'job_template', 'inventory', - 'project', 'credential') + qs = qs.select_related('created_by', 'modified_by', 'job_template', 'inventory', + 'project', 'credential', 'cloud_credential', 'job_template') + qs = qs.prefetch_related('unified_job_template') if self.user.is_superuser: return qs - credential_ids = [x.id for x in self.user.get_queryset(Credential)] + credential_ids = set(self.user.get_queryset(Credential).values_list('id', flat=True)) base_qs = qs.filter( - credential__in=credential_ids, + credential_id__in=credential_ids, ) org_admin_qs = base_qs.filter( project__organizations__admins__in=[self.user] @@ -1045,18 +1040,18 @@ class JobAccess(BaseAccess): # inventory__permissions__pk=F('project__permissions__pk'), # ) - team_ids = [i.id for i in Team.objects.filter(users__in=[self.user])] + team_ids = set(Team.objects.filter(users__in=[self.user]).values_list('id', flat=True)) - deploy_permissions_ids = [i.id for i in Permission.objects.filter( + deploy_permissions_ids = set(Permission.objects.filter( Q(user=self.user) | Q(team__in=team_ids), active=True, permission_type__in=allowed_deploy, - )] - check_permissions_ids = [i.id for i in Permission.objects.filter( + ).values_list('id', flat=True)) + check_permissions_ids = set(Permission.objects.filter( Q(user=self.user) | Q(team__in=team_ids), active=True, permission_type__in=allowed_check, - )] + ).values_list('id', flat=True)) perm_deploy_qs = base_qs.filter( job_type=PERM_INVENTORY_DEPLOY, @@ -1172,7 +1167,7 @@ class JobHostSummaryAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.distinct() - qs = qs.select_related('created_by', 'job', 'job__job_template', + qs = qs.select_related('created_by', 'modified_by', 'job', 'job__job_template', 'host') if self.user.is_superuser: return qs @@ -1198,7 +1193,7 @@ class JobEventAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.distinct() - qs = qs.select_related('created_by', 'job', 'job__job_template', + qs = qs.select_related('created_by', 'modified_by', 'job', 'job__job_template', 'host', 'parent') qs = qs.prefetch_related('hosts', 'children') @@ -1242,7 +1237,17 @@ class UnifiedJobTemplateAccess(BaseAccess): qs = qs.filter(Q(Project___in=project_qs) | Q(InventorySource___in=inventory_source_qs) | Q(JobTemplate___in=job_template_qs)) - # FIXME: select/prefetch to optimize! + qs = qs.select_related( + 'created_by', + 'modified_by', + 'project', + 'inventory', + 'credential', + 'cloud_credential', + 'next_schedule', + 'last_job', + 'current_job', + ) return qs class UnifiedJobAccess(BaseAccess): @@ -1263,7 +1268,21 @@ class UnifiedJobAccess(BaseAccess): Q(InventoryUpdate___in=inventory_update_qs) | Q(Job___in=job_qs) | Q(SystemJob___in=system_job_qs)) - # FIXME: select/prefetch to optimize! + qs = qs.select_related( + 'created_by', + 'modified_by', + 'project', + 'inventory', + 'credential', + 'project___credential', + 'inventory_source___credential', + 'inventory_source___inventory', + 'job_template___inventory', + 'job_template___project', + 'job_template___credential', + 'job_template___cloud_credential', + ) + qs = qs.prefetch_related('unified_job_template') return qs class ScheduleAccess(BaseAccess): @@ -1275,7 +1294,8 @@ class ScheduleAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.filter(active=True).distinct() - qs = qs.select_related('unified_job_template') + qs = qs.select_related('created_by', 'modified_by') + qs = qs.prefetch_related('unified_job_template') if self.user.is_superuser: return qs job_template_qs = self.user.get_queryset(JobTemplate) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 97c8720aae..dd5ea06933 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -137,6 +137,13 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): editable=False, ) + def get_absolute_url(self): + real_instance = self.get_real_instance() + if real_instance != self: + return real_instance.get_absolute_url() + else: + return '' + def unique_error_message(self, model_class, unique_check): # If polymorphic_ctype is part of a unique check, return a list of the # remaining fields instead of the error message. @@ -438,6 +445,13 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique editable=False, ) + def get_absolute_url(self): + real_instance = self.get_real_instance() + if real_instance != self: + return real_instance.get_absolute_url() + else: + return '' + @classmethod def _get_task_class(cls): raise NotImplementedError # Implement in subclasses. From 045592e1ebad4015938baea88bc094fab30bc12b Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 25 Nov 2014 08:32:45 -0500 Subject: [PATCH 10/12] Launch configuration/survey taker Fixed issue with optional text/textarea/mc questions being required --- awx/ui/static/js/helpers/JobSubmission.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index 447f33d845..c274029cf9 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -447,10 +447,9 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi question.index = index; requiredAsterisk = (question.required===true) ? "prepend-asterisk" : ""; - + requiredClasses = (question.required===true) ? "ng-pristine ng-invalid-required ng-invalid" : ""; html+='
'; - requiredClasses = (question.required===true) ? "ng-pristine ng-invalid-required ng-invalid" : ""; html += '\n'; if(!Empty(question.question_description)){ @@ -461,7 +460,7 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi if(question.type === 'text' ){ html+=''+ + 'class="form-control" ng-required='+question.required+'>'+ '
A value is required!
'+ '
'; @@ -470,7 +469,7 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi if(question.type === "textarea"){ scope[question.variable] = question.default || question.default_textarea; html+=''+ + 'class="form-control final" ng-required="'+question.required+'" rows="3">'+ '
A value is required!
'+ '
'; @@ -483,7 +482,7 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi html+='
'; for( j = 0; j' + + html+= '' + ''+choices[j] +'
' ; } html+= '
'+ + html+=''+ + '
A value is required!
'+ + '
'+ '
This is not valid integer!
'+ '
The value must be in range {{'+min+'}} to {{'+max+'}}!
'; + } if(question.type === "float"){ min = (!Empty(question.min)) ? question.min : ""; max = (!Empty(question.max)) ? question.max : "" ; defaultValue = (!Empty(question.default)) ? question.default : (!Empty(question.default_float)) ? question.default_float : "" ; - html+=''+ + html+=''+ '
This is not valid float!
'+ '
The value must be in range {{'+min+'}} to {{'+max+'}}!
'; + // '
A value is required!
'; } html+='
'; if(question.index === scope.survey_questions.length-1){ From 9f6e74ac9497ab67ded73ee848c589a6a83ab691 Mon Sep 17 00:00:00 2001 From: James Laska Date: Tue, 25 Nov 2014 09:21:30 -0500 Subject: [PATCH 11/12] Remove tools from .gitignore Not sure why this was here. It had to be ignored when commiting changes to various files under tools. --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 052c86080c..5994375544 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ awx/tower_warnings.log tower/tower_warnings.log celerybeat-schedule awx/ui/static/docs -tools # Python & setuptools __pycache__ From d47d2f57000b60e11e3294af954aeb5452309f33 Mon Sep 17 00:00:00 2001 From: James Laska Date: Tue, 25 Nov 2014 09:22:02 -0500 Subject: [PATCH 12/12] Add ansible-tower.{default,sysconfig} script The 'ansible-tower' service script now comes with an additional file: /etc/{default,sysconfig}/ansible-tower This file is used to specify the services managed by the 'ansible-tower' service script. The presence of this file allows admins (or the setup playbook) to customize the services managed. For example, when using a remote postgres server, one would remove 'postgresql' from the list of tower managed services. --- Makefile | 1 + tools/scripts/ansible-tower | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 5845f5ad6f..23c57dc249 100644 --- a/Makefile +++ b/Makefile @@ -312,6 +312,7 @@ rpm-build/$(SDIST_TAR_FILE): dist/$(SDIST_TAR_FILE) mkdir -p rpm-build cp packaging/rpm/$(NAME).spec rpm-build/ cp packaging/rpm/$(NAME).te rpm-build/ + cp packaging/rpm/$(NAME).sysconfig rpm-build/ cp packaging/remove_tower_source.py rpm-build/ if [ "$(OFFICIAL)" != "yes" ] ; then \ (cd dist/ && tar zxf $(SDIST_TAR_FILE)) ; \ diff --git a/tools/scripts/ansible-tower b/tools/scripts/ansible-tower index 049cc32bd4..3e3eb86991 100755 --- a/tools/scripts/ansible-tower +++ b/tools/scripts/ansible-tower @@ -18,18 +18,16 @@ # Default configured services if [ -e /etc/debian_version ]; then - SERVICES="postgresql redis-server apache2 supervisor" - TOWER_CONFIG="/etc/default/tower" + TOWER_CONFIG="/etc/default/ansible-tower" else - SERVICES="postgresql redis httpd supervisord" - TOWER_CONFIG="/etc/sysconfig/tower" + TOWER_CONFIG="/etc/sysconfig/ansible-tower" fi -# Load any configuration +# Load default configuration [ -e "${TOWER_CONFIG}" ] && . "${TOWER_CONFIG}" service_action() { - for svc in ${SERVICES}; do + for svc in ${TOWER_SERVICES}; do service ${svc} $1 this_return=$? if [ $this_return -gt $worst_return ]; then