From e67dfa3b92b19a9aae969f9004624fdd75776207 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 16 Apr 2015 17:48:27 -0400 Subject: [PATCH 1/9] openstack inventory ui support --- awx/ui/static/js/forms/Source.js | 4 +- awx/ui/static/js/helpers/Groups.js | 211 ++++++++++++---------- awx/ui/static/js/lists/HomeGroups.js | 7 +- awx/ui/static/js/lists/InventoryGroups.js | 5 +- 4 files changed, 126 insertions(+), 101 deletions(-) diff --git a/awx/ui/static/js/forms/Source.js b/awx/ui/static/js/forms/Source.js index 66d32dac3d..c09ed5d0e6 100644 --- a/awx/ui/static/js/forms/Source.js +++ b/awx/ui/static/js/forms/Source.js @@ -164,7 +164,9 @@ export default }, inventory_variables: { label: 'Source Variables', //"{{vars_label}}" , - ngShow: "source && (source.value == 'vmware')", + + ngShow: "source && (source.value == 'vmware' || " + + "source.value == 'openstack')", type: 'textarea', addRequired: false, editRequird: false, diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index f186942701..ee40eb548c 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -1,5 +1,5 @@ /********************************************* - * Copyright (c) 2014 AnsibleWorks, Inc. + * Copyright (c) 2015 AnsibleWorks, Inc. * * GroupsHelper * @@ -232,102 +232,117 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name ]) +/** + * + * TODO: Document + * + */ +.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, invUrl; -.factory('SourceChange', ['GetBasePath', 'CredentialList', 'LookUpInit', 'Empty', 'Wait', 'ParseTypeChange', 'CustomInventoryList', 'CreateSelect2', - function (GetBasePath, CredentialList, LookUpInit, Empty, Wait, ParseTypeChange, CustomInventoryList, CreateSelect2) { - return function (params) { - - var scope = params.scope, - form = params.form, - kind, url, callback, invUrl; - - - if (!Empty(scope.source)) { - if (scope.source.value === 'file') { - scope.sourcePathRequired = true; - } else { - scope.sourcePathRequired = false; - // reset fields - scope.source_path = ''; - scope[form.name + '_form'].source_path.$setValidity('required', true); - } - if (scope.source.value === 'rax') { - scope.source_region_choices = scope.rax_regions; - $('#source_form').addClass('squeeze'); - CreateSelect2({ - element: '#source_source_regions' - }); - } - else if (scope.source.value === 'ec2') { - scope.source_region_choices = scope.ec2_regions; - scope.group_by_choices = scope.ec2_group_by; - $('#source_form').addClass('squeeze'); - CreateSelect2({ - element: '#source_source_regions' - }); - CreateSelect2({ - element: '#source_group_by' - }); - - } - else if (scope.source.value === 'gce') { - scope.source_region_choices = scope.gce_regions; - $('#source_form').addClass('squeeze'); - CreateSelect2({ - element: '#source_source_regions' - }); - - } else if (scope.source.value === 'azure') { - scope.source_region_choices = scope.azure_regions; - $('#source_form').addClass('squeeze'); - CreateSelect2({ - element: '#source_source_regions' - }); - } - if(scope.source.value==="custom"){ - // need to filter the possible custom scripts by the organization defined for the current inventory - invUrl = GetBasePath('inventory_scripts') + '?organization='+scope.$parent.inventory.organization; - LookUpInit({ - url: invUrl, - scope: scope, - form: form, - hdr: "Select Custom Inventory", - 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==="vmware"){ - scope.inventory_variables = (Empty(scope.source_vars)) ? "---" : scope.source_vars; - ParseTypeChange({ scope: scope, variable: 'inventory_variables', parse_variable: form.fields.inventory_variables.parseTypeName, - field_id: 'source_inventory_variables', 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; - LookUpInit({ - url: url, - scope: scope, - form: form, - list: CredentialList, - field: 'credential', - input_type: "radio" - }); - if ($('#group_tabs .active a').text() === 'Source' && (scope.source.value === 'ec2' )) { - callback = function(){ Wait('stop'); }; - Wait('start'); - scope.source_vars = (Empty(scope.source_vars)) ? "---" : scope.source_vars; - ParseTypeChange({ scope: scope, variable: 'source_vars', parse_variable: form.fields.source_vars.parseTypeName, - field_id: 'source_source_vars', onReady: callback }); - } - } - } - }; - } + if (!Empty(scope.source)) { + if (scope.source.value === 'file') { + scope.sourcePathRequired = true; + } else { + scope.sourcePathRequired = false; + // reset fields + scope.source_path = ''; + scope[form.name + '_form'].source_path.$setValidity('required', true); + } + if (scope.source.value === 'rax') { + scope.source_region_choices = scope.rax_regions; + //$('#s2id_group_source_regions').select2('data', []); + $('#s2id_source_source_regions').select2('data', [{ + id: 'all', + text: 'All' + }]); + $('#source_form').addClass('squeeze'); + } else if (scope.source.value === 'ec2') { + scope.source_region_choices = scope.ec2_regions; + $('#s2id_source_source_regions').select2('data', [{ + id: 'all', + text: 'All' + }]); + scope.group_by_choices = scope.ec2_group_by; + $('#s2id_group_by').select2('data', []); + $('#source_form').addClass('squeeze'); + } else if (scope.source.value === 'gce') { + scope.source_region_choices = scope.gce_regions; + //$('#s2id_group_source_regions').select2('data', []); + $('#s2id_source_source_regions').select2('data', [{ + id: 'all', + text: 'All' + }]); + $('#source_form').addClass('squeeze'); + } else if (scope.source.value === 'azure') { + scope.source_region_choices = scope.azure_regions; + //$('#s2id_group_source_regions').select2('data', []); + $('#s2id_source_source_regions').select2('data', [{ + id: 'all', + text: 'All' + }]); + $('#source_form').addClass('squeeze'); + } + if(scope.source.value==="custom"){ + // need to filter the possible custom scripts by the organization defined for the current inventory + invUrl = GetBasePath('inventory_scripts') + '?organization='+scope.$parent.inventory.organization; + LookUpInit({ + url: invUrl, + scope: scope, + form: form, + hdr: "Select Custom Inventory", + 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==="vmware" + || scope.source.value==="openstack"){ + scope.inventory_variables = (Empty(scope.source_vars)) ? "---" : scope.source_vars; + ParseTypeChange({ scope: scope, variable: 'inventory_variables', parse_variable: form.fields.inventory_variables.parseTypeName, + field_id: 'source_inventory_variables', onReady: callback }); + } + if (scope.source.value === 'rax' + || scope.source.value === 'ec2' + || scope.source.value==='gce' + || scope.source.value === 'azure' + || scope.source.value === 'vmware' + || scope.source.value === 'openstack') { + if (scope.source.value === 'ec2') { + kind = 'aws'; + } else { + kind = scope.source.value; + } + url = GetBasePath('credentials') + '?cloud=true&kind=' + kind; + LookUpInit({ + url: url, + scope: scope, + form: form, + list: CredentialList, + field: 'credential', + input_type: "radio" + }); + if ($('#group_tabs .active a').text() === 'Source' + && (scope.source.value === 'ec2' )) { + callback = function(){ Wait('stop'); }; + Wait('start'); + scope.source_vars = (Empty(scope.source_vars)) ? "---" : scope.source_vars; + ParseTypeChange({ scope: scope, variable: 'source_vars', + parse_variable: form.fields.source_vars.parseTypeName, + field_id: 'source_source_vars', onReady: callback }); + } + } + } + }; + } ]) /** @@ -903,7 +918,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name Wait('start'); ParseTypeChange({ scope: sources_scope, variable: 'source_vars', parse_variable: SourceForm.fields.source_vars.parseTypeName, field_id: 'source_source_vars', onReady: waitStop }); - } else if (sources_scope.source && (sources_scope.source.value === 'vmware')) { + } else if (sources_scope.source && (sources_scope.source.value === 'vmware' + || sources_scope.source.value === 'openstack')) { Wait('start'); ParseTypeChange({ scope: sources_scope, variable: 'inventory_variables', parse_variable: SourceForm.fields.inventory_variables.parseTypeName, field_id: 'source_inventory_variables', onReady: waitStop }); @@ -1282,7 +1298,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.extra_vars, true); } - if (sources_scope.source && (sources_scope.source.value === 'vmware')) { + if (sources_scope.source && (sources_scope.source.value === 'vmware' + || sources_scope.source.value === 'openstack')) { data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.inventory_variables, true); } diff --git a/awx/ui/static/js/lists/HomeGroups.js b/awx/ui/static/js/lists/HomeGroups.js index 4d6f15a23f..0d1d294cc2 100644 --- a/awx/ui/static/js/lists/HomeGroups.js +++ b/awx/ui/static/js/lists/HomeGroups.js @@ -78,6 +78,9 @@ export default },{ name: "Microsoft Azure", value: "azure" + },{ + name: "Openstack", + value: "openstack" }], sourceModel: 'inventory_source', sourceField: 'source', @@ -86,7 +89,7 @@ export default has_external_source: { label: 'Has external source?', searchType: 'in', - searchValue: 'ec2,rax,vmware,azure,gce', + searchValue: 'ec2,rax,vmware,azure,gce,openstack', searchOnly: true, sourceModel: 'inventory_source', sourceField: 'source' @@ -170,4 +173,4 @@ export default } } - }); \ No newline at end of file + }); diff --git a/awx/ui/static/js/lists/InventoryGroups.js b/awx/ui/static/js/lists/InventoryGroups.js index d7c0f9d972..8d283cfa61 100644 --- a/awx/ui/static/js/lists/InventoryGroups.js +++ b/awx/ui/static/js/lists/InventoryGroups.js @@ -47,6 +47,9 @@ export default },{ name: "Microsoft Azure", value: "azure" + },{ + name: "Openstack", + value: "openstack" }], sourceModel: 'inventory_source', sourceField: 'source', @@ -55,7 +58,7 @@ export default has_external_source: { label: 'Has external source?', searchType: 'in', - searchValue: 'ec2,rax,vmware,azure,gce', + searchValue: 'ec2,rax,vmware,azure,gce,openstack', searchOnly: true, sourceModel: 'inventory_source', sourceField: 'source' From 40b26e42bb0540723ac08e2f8f91cd3884262361 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 20 Apr 2015 11:13:35 -0400 Subject: [PATCH 2/9] fixed padding for cancel button in lists --- awx/ui/static/less/ansible-ui.less | 2 -- 1 file changed, 2 deletions(-) diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index e956e799e0..44d01db92c 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -1487,8 +1487,6 @@ input[type="checkbox"].checkbox-no-label { border-right: 1px solid #ddd; } - #groups_table .actions .cancel { padding-right: 2px; } - .node-toggle, .node-no-toggle { /* also used on job evetns */ float: none; From 22acd51650892d165cb9dbc4b1fb2298dccc3cf0 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 24 Apr 2015 14:15:25 -0400 Subject: [PATCH 3/9] Implementing tower cleanup task for cleaning up facts --- awx/main/management/commands/cleanup_facts.py | 4 ++-- awx/main/models/jobs.py | 1 + awx/main/tasks.py | 9 +++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/awx/main/management/commands/cleanup_facts.py b/awx/main/management/commands/cleanup_facts.py index cd427f082a..d127b52c52 100644 --- a/awx/main/management/commands/cleanup_facts.py +++ b/awx/main/management/commands/cleanup_facts.py @@ -4,12 +4,12 @@ # Python import re from dateutil.relativedelta import relativedelta -from datetime import datetime from optparse import make_option # Django from django.core.management.base import BaseCommand, CommandError from django.db import transaction +from django.utils.timezone import now # AWX from awx.fact.models.fact import * # noqa @@ -72,7 +72,7 @@ class CleanupFacts(object): older_than and granularity are of type relativedelta ''' def run(self, older_than, granularity): - t = datetime.now() + t = now() deleted_count = self.cleanup(t - older_than, granularity) print("Deleted %d facts." % deleted_count) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 78b45f5d12..0ab8d0ee67 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -945,6 +945,7 @@ class SystemJobOptions(BaseModel): ('cleanup_jobs', _('Remove jobs older than a certain number of days')), ('cleanup_activitystream', _('Remove activity stream entries older than a certain number of days')), ('cleanup_deleted', _('Purge previously deleted items from the database')), + ('cleanup_facts', _('Purge and/or reduce the granularity of system tracking data')), ] class Meta: diff --git a/awx/main/tasks.py b/awx/main/tasks.py index adf60f9516..3e346861d4 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1352,10 +1352,15 @@ class RunSystemJob(BaseTask): args = ['awx-manage', system_job.job_type] try: json_vars = json.loads(system_job.extra_vars) - if 'days' in json_vars: - args.extend(['--days', str(json_vars['days'])]) + if 'days' in json_vars and system_job.job_type != 'cleanup_facts': + args.extend(['--days', str(json_vars.get('days', 60))]) if system_job.job_type == 'cleanup_jobs': args.extend(['--jobs', '--project-updates', '--inventory-updates', '--management-jobs']) + if system_job.job_type == 'cleanup_facts': + if 'older_than' in json_vars: + args.extend(['--older_than', str(json_vars['older_than'])]) + if 'granularity' in json_vars: + args.extend(['--granularity', str(json_vars['granularity'])]) # Keeping this around in case we want to break this out # if 'jobs' in json_vars and json_vars['jobs']: # args.extend(['--jobs']) From 4bdab2d2fab15b8673d1117feca4e43b811f3d8c Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 27 Apr 2015 10:13:25 -0400 Subject: [PATCH 4/9] adhoc form fixes added forks input fixed privilege escalation language, it now mimics job template fixed a hanging issue when a non-machine credential is used fixed reset to include the default verbosity setting --- awx/ui/static/js/controllers/Adhoc.js | 46 +++++++---------------- awx/ui/static/js/forms/Adhoc.js | 39 ++++++++++++++----- awx/ui/static/js/forms/JobTemplates.js | 2 +- awx/ui/static/js/helpers/JobSubmission.js | 3 +- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/awx/ui/static/js/controllers/Adhoc.js b/awx/ui/static/js/controllers/Adhoc.js index d4d3501f2d..d9e5779262 100644 --- a/awx/ui/static/js/controllers/Adhoc.js +++ b/awx/ui/static/js/controllers/Adhoc.js @@ -36,6 +36,7 @@ export function AdhocCtrl($scope, $rootScope, $location, $routeParams, $scope.id = id; $scope.argsPopOver = "

These arguments are used with the" + " specified module.

"; + // fix arguments help popover based on the module selected $scope.moduleChange = function () { // NOTE: for selenium testing link - @@ -64,20 +65,13 @@ export function AdhocCtrl($scope, $rootScope, $location, $routeParams, $scope.providedHostPatterns = $scope.limit; delete $rootScope.hostPatterns; - if ($scope.removeLookUpInitialize) { - $scope.removeLookUpInitialize(); - } - $scope.removeLookUpInitialize = $scope.$on('lookUpInitialize', function () { - LookUpInit({ - scope: $scope, - form: form, - current_item: (!Empty($scope.credential_id)) ? $scope.credential_id : null, - list: CredentialList, - field: 'credential', - input_type: 'radio' - }); - - Wait('stop'); // END: form population + LookUpInit({ + scope: $scope, + form: form, + current_item: (!Empty($scope.credential_id)) ? $scope.credential_id : null, + list: CredentialList, + field: 'credential', + input_type: 'radio' }); if ($scope.removeChoicesReady) { @@ -87,9 +81,10 @@ export function AdhocCtrl($scope, $rootScope, $location, $routeParams, choicesReadyCount++; if (choicesReadyCount === 2) { - // this sets the default option as specified by the controller. + // this sets the default options for the selects as specified by the controller. $scope.verbosity = $scope.adhoc_verbosity_options[$scope.verbosity_field.default]; - $scope.$emit('lookUpInitialize'); + $("#forks-number").spinner("value", $scope.forks_field.default); + Wait('stop'); // END: form population } }); @@ -212,23 +207,6 @@ export function AdhocCtrl($scope, $rootScope, $location, $routeParams, credential: $scope.credential, callback: 'ContinueCred' }); - - // // Launch the adhoc job - // Rest.setUrl(url); - // Rest.post(data) - // .success(function (data) { - // Wait('stop'); - // $location.path("/ad_hoc_commands/" + data.id); - // }) - // .error(function (data, status) { - // ProcessErrors($scope, data, status, form, { hdr: 'Error!', - // msg: 'Failed to launch adhoc command. POST returned status: ' + - // status }); - // // TODO: still need to implement popping up a password prompt - // // if the credential requires it. The way that the current end- - // // point works is that I find out if I need to ask for a - // // password from POST, thus I get an error response. - // }); }; // Remove all data input into the form @@ -239,6 +217,8 @@ export function AdhocCtrl($scope, $rootScope, $location, $routeParams, } $scope.limit = $scope.providedHostPatterns; KindChange({ scope: $scope, form: form, reset: false }); + $scope.verbosity = $scope.adhoc_verbosity_options[$scope.verbosity_field.default]; + $("#forks-number").spinner("value", $scope.forks_field.default); }; } diff --git a/awx/ui/static/js/forms/Adhoc.js b/awx/ui/static/js/forms/Adhoc.js index 62c66d342a..3c83aa78c8 100644 --- a/awx/ui/static/js/forms/Adhoc.js +++ b/awx/ui/static/js/forms/Adhoc.js @@ -82,14 +82,15 @@ export default } }, become_enabled: { - label: 'Enable Become for Credential', + label: 'Enable Privilege Escalation', type: 'checkbox', - editRequired: false - // awPopOver: '

If checked, user will be become the user ' + - // 'specified by the credential.

', - // dataPlacement: 'right', - // dataTitle: 'Enable Become for Credential', - // dataContainer: 'body' + addRequired: false, + editRequird: false, + column: 2, + awPopOver: "

If enabled, run this playbook as an administrator. This is the equivalent of passing the --become option to the ansible command.

", + dataPlacement: 'right', + dataTitle: 'Become Privilege Escalation', + dataContainer: "body" }, verbosity: { label: 'Verbosity', @@ -104,8 +105,28 @@ export default 'out of the command run that are supported.', dataTitle: 'Module', dataPlacement: 'right', - dataContainer: 'body' - } + dataContainer: 'body', + default: 1 + }, + forks: { + label: 'Forks', + id: 'forks-number', + type: 'number', + integer: true, + min: 0, + spinner: true, + "default": 0, + addRequired: false, + editRequired: false, + 'class': "input-small", + column: 1, + awPopOver: '

The number of parallel or simultaneous processes to use while executing the command. 0 signifies ' + + 'the default value from the ansible configuration file.

', + dataTitle: 'Forks', + dataPlacement: 'right', + dataContainer: "body" + }, }, buttons: { diff --git a/awx/ui/static/js/forms/JobTemplates.js b/awx/ui/static/js/forms/JobTemplates.js index be05435de3..49f3b70f9f 100644 --- a/awx/ui/static/js/forms/JobTemplates.js +++ b/awx/ui/static/js/forms/JobTemplates.js @@ -176,7 +176,7 @@ export default 'class': "input-small", column: 1, awPopOver: '

The number of parallel or simultaneous processes to use while executing the playbook. 0 signifies ' + - 'the default value from the ansible configuration file.

', dataTitle: 'Forks', dataPlacement: 'right', diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index b7db81955b..f0d9f4bc83 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -710,9 +710,8 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, if(data.vault_password === "ASK"){ passwords.push("vault_password"); } - scope.$emit(callback, passwords); } - + scope.$emit(callback, passwords); }) .error(function (data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', From 2522160f96dbc028e2016ab7c745a16fbe208481 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 27 Apr 2015 10:20:37 -0400 Subject: [PATCH 5/9] fixed grunt errors --- awx/ui/static/js/helpers/Groups.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index ee40eb548c..f4d04503e8 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -304,18 +304,18 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name ParseTypeChange({ scope: scope, variable: 'extra_vars', parse_variable: form.fields.extra_vars.parseTypeName, field_id: 'source_extra_vars', onReady: callback }); } - if(scope.source.value==="vmware" - || scope.source.value==="openstack"){ + if(scope.source.value==="vmware" || + scope.source.value==="openstack"){ scope.inventory_variables = (Empty(scope.source_vars)) ? "---" : scope.source_vars; ParseTypeChange({ scope: scope, variable: 'inventory_variables', parse_variable: form.fields.inventory_variables.parseTypeName, field_id: 'source_inventory_variables', onReady: callback }); } - if (scope.source.value === 'rax' - || scope.source.value === 'ec2' - || scope.source.value==='gce' - || scope.source.value === 'azure' - || scope.source.value === 'vmware' - || scope.source.value === 'openstack') { + if (scope.source.value === 'rax' || + scope.source.value === 'ec2' || + scope.source.value==='gce' || + scope.source.value === 'azure' || + scope.source.value === 'vmware' || + scope.source.value === 'openstack') { if (scope.source.value === 'ec2') { kind = 'aws'; } else { @@ -330,8 +330,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name field: 'credential', input_type: "radio" }); - if ($('#group_tabs .active a').text() === 'Source' - && (scope.source.value === 'ec2' )) { + if ($('#group_tabs .active a').text() === 'Source' && + (scope.source.value === 'ec2' )) { callback = function(){ Wait('stop'); }; Wait('start'); scope.source_vars = (Empty(scope.source_vars)) ? "---" : scope.source_vars; @@ -918,8 +918,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name Wait('start'); ParseTypeChange({ scope: sources_scope, variable: 'source_vars', parse_variable: SourceForm.fields.source_vars.parseTypeName, field_id: 'source_source_vars', onReady: waitStop }); - } else if (sources_scope.source && (sources_scope.source.value === 'vmware' - || sources_scope.source.value === 'openstack')) { + } else if (sources_scope.source && (sources_scope.source.value === 'vmware' || + sources_scope.source.value === 'openstack')) { Wait('start'); ParseTypeChange({ scope: sources_scope, variable: 'inventory_variables', parse_variable: SourceForm.fields.inventory_variables.parseTypeName, field_id: 'source_inventory_variables', onReady: waitStop }); @@ -1298,8 +1298,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.extra_vars, true); } - if (sources_scope.source && (sources_scope.source.value === 'vmware' - || sources_scope.source.value === 'openstack')) { + if (sources_scope.source && (sources_scope.source.value === 'vmware' || + sources_scope.source.value === 'openstack')) { data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.inventory_variables, true); } From 0f30ccc9b73ce57c1758d06b67d234fa88145441 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 27 Apr 2015 10:36:49 -0400 Subject: [PATCH 6/9] fix grunt errors --- awx/ui/static/js/forms/Adhoc.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/awx/ui/static/js/forms/Adhoc.js b/awx/ui/static/js/forms/Adhoc.js index 3c83aa78c8..c64c90536a 100644 --- a/awx/ui/static/js/forms/Adhoc.js +++ b/awx/ui/static/js/forms/Adhoc.js @@ -99,14 +99,13 @@ export default ngOptions: 'verbosity.label for verbosity in ' + 'adhoc_verbosity_options ' + 'track by verbosity.value', - "default": 1, editRequired: true, awPopOver:'

These are the verbosity levels for standard ' + 'out of the command run that are supported.', dataTitle: 'Module', dataPlacement: 'right', dataContainer: 'body', - default: 1 + "default": 1 }, forks: { label: 'Forks', From f1afae4b8098cac75b0cd61c01e32b38fc0ae834 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 27 Apr 2015 10:36:35 -0400 Subject: [PATCH 7/9] support --granularity=0d to delete all facts. Still respect --older_than --- awx/main/management/commands/cleanup_facts.py | 17 +++++++++++++++-- awx/main/tests/commands/cleanup_facts.py | 6 ++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/awx/main/management/commands/cleanup_facts.py b/awx/main/management/commands/cleanup_facts.py index cd427f082a..496a0a9de5 100644 --- a/awx/main/management/commands/cleanup_facts.py +++ b/awx/main/management/commands/cleanup_facts.py @@ -30,20 +30,32 @@ class CleanupFacts(object): # pivot -= granularity # group by host def cleanup(self, older_than_abs, granularity): + flag_delete_all = False fact_oldest = FactVersion.objects.all().order_by('timestamp').first() if not fact_oldest: return 0 + # Special case, granularity=0x where x is d, w, or y + # The intent is to delete all facts < older_than_abs + if granularity == relativedelta(): + flag_delete_all = True + total = 0 date_pivot = older_than_abs while date_pivot > fact_oldest.timestamp: date_pivot_next = date_pivot - granularity kv = { - 'timestamp__lte': date_pivot, - 'timestamp__gt': date_pivot_next, + 'timestamp__lte': date_pivot } + if not flag_delete_all: + kv['timestamp__gt'] = date_pivot_next + version_objs = FactVersion.objects.filter(**kv).order_by('-timestamp') + if flag_delete_all: + total = version_objs.delete() + break + # Transform array -> {host_id} = [, , ...] # TODO: If this set gets large then we can use mongo to transform the data set for us. host_ids = {} @@ -66,6 +78,7 @@ class CleanupFacts(object): total += count date_pivot = date_pivot_next + return total ''' diff --git a/awx/main/tests/commands/cleanup_facts.py b/awx/main/tests/commands/cleanup_facts.py index c10f5cd90b..8d02310a2d 100644 --- a/awx/main/tests/commands/cleanup_facts.py +++ b/awx/main/tests/commands/cleanup_facts.py @@ -28,6 +28,12 @@ class CleanupFactsCommandFunctionalTest(BaseCommandMixin, BaseTest, MongoDBRequi result, stdout, stderr = self.run_command('cleanup_facts', granularity='1w',older_than='5d') self.assertEqual(stdout, 'Deleted 0 facts.\n') + def test_invoke_all_deleted(self): + self.create_hosts_and_facts(datetime(year=2015, day=2, month=1, microsecond=0), 10, 20) + + result, stdout, stderr = self.run_command('cleanup_facts', granularity='0d', older_than='0d') + self.assertEqual(stdout, 'Deleted 200 facts.\n') + def test_invoke_params_required(self): result, stdout, stderr = self.run_command('cleanup_facts') self.assertIsInstance(result, CommandError) From d1ea8708ad1d1145a0c15beb26877f160a4a9c9f Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 27 Apr 2015 12:42:40 -0400 Subject: [PATCH 8/9] Make sure credential can only be assigned to a user OR team, but never both. Fixes https://trello.com/c/yzlAEfAN --- awx/api/generics.py | 4 ++-- awx/api/serializers.py | 10 ++++++++++ awx/main/models/credential.py | 17 +++++++++++++++++ awx/main/tests/projects.py | 27 ++++++++++++++++++++++++++- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 78071f2222..1ac01a3eb6 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -365,7 +365,7 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView): data[parent_key] = self.kwargs['pk'] # attempt to deserialize the object - serializer = self.serializer_class(data=data) + serializer = self.get_serializer(data=data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -377,7 +377,7 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView): # save the object through the serializer, reload and returned the saved # object deserialized obj = serializer.save() - serializer = self.serializer_class(obj) + serializer = self.get_serializer(instance=obj) headers = {'Location': obj.get_absolute_url()} return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 5bf92ab2b3..6e361724ea 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1295,6 +1295,16 @@ class CredentialSerializer(BaseSerializer): for field in Credential.PASSWORD_FIELDS: if unicode(attrs.get(field, '')).startswith('$encrypted$'): attrs.pop(field, None) + + # If creating a credential from a view that automatically sets the + # parent_key (user or team), set the other value to None. + view = self.context.get('view', None) + parent_key = getattr(view, 'parent_key', None) + if parent_key == 'user': + attrs['team'] = None + if parent_key == 'team': + attrs['user'] = None + instance = super(CredentialSerializer, self).restore_object(attrs, instance) return instance diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 1bce98d643..2975baf6d2 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -379,6 +379,23 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): # If update_fields has been specified, add our field names to it, # if hit hasn't been specified, then we're just doing a normal save. update_fields = kwargs.get('update_fields', []) + # If updating a credential, make sure that we only allow user OR team + # to be set, and clear out the other field based on which one has + # changed. + if self.pk: + cred_before = Credential.objects.get(pk=self.pk) + if self.user and self.team: + # If the user changed, remove the previously assigned team. + if cred_before.user != self.user: + self.team = None + if 'team' not in update_fields: + update_fields.append('team') + # If the team changed, remove the previously assigned user. + elif cred_before.team != self.team: + self.user = None + if 'user' not in update_fields: + update_fields.append('user') + # Set cloud flag based on credential kind. cloud = self.kind in CLOUD_PROVIDERS + ('aws',) if self.cloud != cloud: self.cloud = cloud diff --git a/awx/main/tests/projects.py b/awx/main/tests/projects.py index b27e4ac710..3792db497b 100644 --- a/awx/main/tests/projects.py +++ b/awx/main/tests/projects.py @@ -486,8 +486,11 @@ class ProjectsTest(BaseTransactionTest): # can add credentials to a user (if user or org admin or super user) self.post(other_creds, data=new_credentials, expect=401) self.post(other_creds, data=new_credentials, expect=401, auth=self.get_invalid_credentials()) + new_credentials['team'] = team.pk result = self.post(other_creds, data=new_credentials, expect=201, auth=self.get_super_credentials()) cred_user = result['id'] + self.assertEqual(result['team'], None) + del new_credentials['team'] new_credentials['name'] = 'credential2' self.post(other_creds, data=new_credentials, expect=201, auth=self.get_normal_credentials()) new_credentials['name'] = 'credential3' @@ -497,9 +500,12 @@ class ProjectsTest(BaseTransactionTest): # can add credentials to a team new_credentials['name'] = 'credential' + new_credentials['user'] = other.pk self.post(team_creds, data=new_credentials, expect=401) self.post(team_creds, data=new_credentials, expect=401, auth=self.get_invalid_credentials()) - self.post(team_creds, data=new_credentials, expect=201, auth=self.get_super_credentials()) + result = self.post(team_creds, data=new_credentials, expect=201, auth=self.get_super_credentials()) + self.assertEqual(result['user'], None) + del new_credentials['user'] new_credentials['name'] = 'credential2' result = self.post(team_creds, data=new_credentials, expect=201, auth=self.get_normal_credentials()) new_credentials['name'] = 'credential3' @@ -611,6 +617,25 @@ class ProjectsTest(BaseTransactionTest): cred_put_t = self.put(edit_creds2, data=d_cred_team, expect=200, auth=self.get_normal_credentials()) self.put(edit_creds2, data=d_cred_team, expect=403, auth=self.get_other_credentials()) + # Reassign credential between team and user. + with self.current_user(self.super_django_user): + self.post(team_creds, data=dict(id=cred_user.pk), expect=204) + response = self.get(edit_creds1) + self.assertEqual(response['team'], team.pk) + self.assertEqual(response['user'], None) + self.post(other_creds, data=dict(id=cred_user.pk), expect=204) + response = self.get(edit_creds1) + self.assertEqual(response['team'], None) + self.assertEqual(response['user'], other.pk) + self.post(other_creds, data=dict(id=cred_team.pk), expect=204) + response = self.get(edit_creds2) + self.assertEqual(response['team'], None) + self.assertEqual(response['user'], other.pk) + self.post(team_creds, data=dict(id=cred_team.pk), expect=204) + response = self.get(edit_creds2) + self.assertEqual(response['team'], team.pk) + self.assertEqual(response['user'], None) + cred_put_t['disassociate'] = 1 team_url = reverse('api:team_credentials_list', args=(cred_put_t['team'],)) self.post(team_url, data=cred_put_t, expect=204, auth=self.get_normal_credentials()) From f297a8d2d001a66ea3e499a4532c7557b6975d89 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 27 Apr 2015 14:31:36 -0400 Subject: [PATCH 9/9] quick fix so that launching job templates with cred works again --- awx/api/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx/api/views.py b/awx/api/views.py index a2b3cd699b..3c87ba613f 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1445,12 +1445,15 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView): if not request.user.can_access(self.model, 'start', obj): raise PermissionDenied() + if 'credential' not in request.DATA and 'credential_id' in request.DATA: + request.DATA['credential'] = request.DATA['credential_id'] + serializer = self.serializer_class(data=request.DATA, context={'obj': obj}) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) kv = { - 'credential': serializer.object.credential, + 'credential': serializer.object.credential.pk, 'extra_vars': serializer.object.extra_vars } new_job = obj.create_unified_job(**kv)