From 36992e47ce084526078f6aab764ef5dd17ffac9e Mon Sep 17 00:00:00 2001 From: James Laska Date: Tue, 25 Nov 2014 11:53:41 -0500 Subject: [PATCH 01/15] Update release history and process docs --- README.md | 6 +++++- docs/release_process.md | 29 +++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1a5d001f81..1671f15860 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Ansible Tower Tower provides a web-based user interface, REST API and task engine built on top of Ansible. -The current version under development is 2.0.0. +The current version under development is 2.1.0. Development releases always use the 'master' branch. @@ -21,6 +21,10 @@ Release schedule * 1.4.10, April 28, 2014. * 1.4.11, May 30, 2014. * 2.0.0, August 19, 2014 +* 2.0.1, September 4, 2014 +* 2.0.2, October 6, 2014 +* 2.0.3, November 14, 2014 +* 2.0.4, November 21, 2014 Any fixes should be applied on the appropriate release branch and be cherry-picked to master. diff --git a/docs/release_process.md b/docs/release_process.md index 059e8bb73c..73599aa673 100644 --- a/docs/release_process.md +++ b/docs/release_process.md @@ -1,21 +1,38 @@ Release Process =============== -This document describes the process of created and publishing an Ansible Tower release. +This document describes the process of creating and publishing an Ansible Tower release. Time for a release ------------------ -When the time comes for a release, the first step is to tag the release in git. +When the time comes for a release, the following steps will ensure a smooth and +successful release. + +1. Verify that the `__version__` variable has been updated in `awx/__init__.py`. + + __version__ = 'X.Y.Z' + +2. Update the rpm package changelog by adding a new entry to the file `packaging/rpm/ansible-tower.spec`. + +3. Update the debian package changelog by adding a new entry to the file `packaging/debian/changelog`. + +4. Tag and push the release to git. # git tag + # git push --tags + +5. Create and push a release branch to git. + + # git branch release_ + # git checkout release_ + # git push origin release_ Monitor Jenkins --------------- -Once tagged, [Jenkins](http://50.116.42.103/view/Tower/) will take care of the -following steps. The jenkins job -[Release_Tower](http://50.116.42.103/view/Tower/job/Release_Tower/) will detect -the recent tag, and initiate the `OFFICIAL=yes` build process. +Once tagged, one must launch the [Release_Tower](http://50.116.42.103/view/Tower/job/Release_Tower/) with the following parameters: +* `GIT_BRANCH=origin/tags/` +* `OFFICIAL=yes` The following jobs will be triggered: * [Build_Tower_TAR](http://50.116.42.103/view/Tower/) From b94e0711c52adabea2ff3a635a9cb8e26632d7fe Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 25 Nov 2014 15:10:50 -0500 Subject: [PATCH 02/15] Host events added check in host event call to make sure an empty data set is not returend from the API --- awx/ui/static/js/helpers/EventViewer.js | 136 ++++++++++++---------- awx/ui/static/js/helpers/JobSubmission.js | 11 +- awx/ui/static/lib/ansible/directives.js | 35 ++++-- 3 files changed, 105 insertions(+), 77 deletions(-) diff --git a/awx/ui/static/js/helpers/EventViewer.js b/awx/ui/static/js/helpers/EventViewer.js index b2882204dc..8a4405e1c8 100644 --- a/awx/ui/static/js/helpers/EventViewer.js +++ b/awx/ui/static/js/helpers/EventViewer.js @@ -273,7 +273,8 @@ angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFo }; }]) - .factory('GetEvent', ['Wait', 'Rest', 'ProcessErrors', function(Wait, Rest, ProcessErrors) { + .factory('GetEvent', ['Wait', 'Rest', 'ProcessErrors', + function(Wait, Rest, ProcessErrors) { return function(params) { var url = params.url, scope = params.scope, @@ -295,70 +296,79 @@ angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFo Rest.setUrl(url); Rest.get() .success( function(data) { - scope.next_event_set = data.next; - scope.prev_event_set = data.previous; - data.results.forEach(function(event) { - var msg, key, event_data = {}; - if (event.event_data.res) { - if (typeof event.event_data.res !== 'object') { - // turn event_data.res into an object - msg = event.event_data.res; - event.event_data.res = {}; - event.event_data.res.msg = msg; - } - for (key in event.event_data) { - if (key !== "res") { - event.event_data.res[key] = event.event_data[key]; - } - } - if (event.event_data.res.ansible_facts) { - // don't show fact gathering results - event.event_data.res.task = "Gathering Facts"; - delete event.event_data.res.ansible_facts; - } - event.event_data.res.status = getStatus(event); - event_data = event.event_data.res; - } - else { - event.event_data.status = getStatus(event); - event_data = event.event_data; - } - // convert results to stdout - if (event_data.results && typeof event_data.results === "object" && Array.isArray(event_data.results)) { - event_data.stdout = ""; - event_data.results.forEach(function(row) { - event_data.stdout += row + "\n"; - }); - delete event_data.results; - } - if (event_data.invocation) { - for (key in event_data.invocation) { - event_data[key] = event_data.invocation[key]; - } - delete event_data.invocation; - } - event_data.play = event.play; - if (event.task) { - event_data.task = event.task; - } - event_data.created = event.created; - event_data.role = event.role; - event_data.host_id = event.host; - event_data.host_name = event.host_name; - if (event_data.host) { - delete event_data.host; - } - event_data.id = event.id; - event_data.parent = event.parent; - event_data.event = (event.event_display) ? event.event_display : event.event; - results.push(event_data); - }); - if (show_event) { - scope.$emit('ShowNextEvent', results, show_event); + + if(jQuery.isEmptyObject(data)) { + Wait('stop'); + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get event ' + url + '. ' }); + } else { - scope.$emit('EventReady', results); - } + scope.next_event_set = data.next; + scope.prev_event_set = data.previous; + data.results.forEach(function(event) { + var msg, key, event_data = {}; + if (event.event_data.res) { + if (typeof event.event_data.res !== 'object') { + // turn event_data.res into an object + msg = event.event_data.res; + event.event_data.res = {}; + event.event_data.res.msg = msg; + } + for (key in event.event_data) { + if (key !== "res") { + event.event_data.res[key] = event.event_data[key]; + } + } + if (event.event_data.res.ansible_facts) { + // don't show fact gathering results + event.event_data.res.task = "Gathering Facts"; + delete event.event_data.res.ansible_facts; + } + event.event_data.res.status = getStatus(event); + event_data = event.event_data.res; + } + else { + event.event_data.status = getStatus(event); + event_data = event.event_data; + } + // convert results to stdout + if (event_data.results && typeof event_data.results === "object" && Array.isArray(event_data.results)) { + event_data.stdout = ""; + event_data.results.forEach(function(row) { + event_data.stdout += row + "\n"; + }); + delete event_data.results; + } + if (event_data.invocation) { + for (key in event_data.invocation) { + event_data[key] = event_data.invocation[key]; + } + delete event_data.invocation; + } + event_data.play = event.play; + if (event.task) { + event_data.task = event.task; + } + event_data.created = event.created; + event_data.role = event.role; + event_data.host_id = event.host; + event_data.host_name = event.host_name; + if (event_data.host) { + delete event_data.host; + } + event_data.id = event.id; + event_data.parent = event.parent; + event_data.event = (event.event_display) ? event.event_display : event.event; + results.push(event_data); + }); + if (show_event) { + scope.$emit('ShowNextEvent', results, show_event); + } + else { + scope.$emit('EventReady', results); + } + } //else statement }) .error(function(data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index c274029cf9..98045da00f 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -517,11 +517,9 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi if(question.type === 'integer'){ min = (!Empty(question.min)) ? Number(question.min) : ""; max = (!Empty(question.max)) ? Number(question.max) : "" ; - html+=''+ - '
A value is required!
'+ - '
'+ - '
This is not valid integer!
'+ + html+=''+ + '
A value is required!
'+ + '
This is not valid integer!
'+ '
The value must be in range {{'+min+'}} to {{'+max+'}}!
'; } @@ -530,10 +528,9 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi 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){ diff --git a/awx/ui/static/lib/ansible/directives.js b/awx/ui/static/lib/ansible/directives.js index a62a1d3367..a67e4ce039 100644 --- a/awx/ui/static/lib/ansible/directives.js +++ b/awx/ui/static/lib/ansible/directives.js @@ -148,6 +148,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job return { restrict: 'A', require: 'ngModel', + // scope: true, link: function (scope, elem, attr, ctrl) { scope.$watch(attr.ngMin, function () { ctrl.$setViewValue(ctrl.$viewValue); @@ -162,7 +163,6 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job return value; } }; - ctrl.$parsers.push(minValidator); ctrl.$formatters.push(minValidator); } @@ -173,6 +173,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job return { restrict: 'A', require: 'ngModel', + // scope: true, link: function (scope, elem, attr, ctrl) { scope.$watch(attr.ngMax, function () { ctrl.$setViewValue(ctrl.$viewValue); @@ -187,7 +188,6 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job return value; } }; - ctrl.$parsers.push(maxValidator); ctrl.$formatters.push(maxValidator); } @@ -196,14 +196,19 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job .directive('smartFloat', function() { - var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/; + var FLOAT_REGEXP_1 = /^\$?\d+(.\d{3})*(\,\d*)?$/, //Numbers like: 1.123,56 + FLOAT_REGEXP_2 = /^\$?\d+(,\d{3})*(\.\d*)?$/; //Numbers like: 1,123.56 return { + restrict: 'A', require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - ctrl.$parsers.unshift(function(viewValue) { - if (FLOAT_REGEXP.test(viewValue)) { + link: function (scope, elm, attrs, ctrl) { + ctrl.$parsers.unshift(function (viewValue) { + if (FLOAT_REGEXP_1.test(Number(viewValue))) { ctrl.$setValidity('float', true); - return parseFloat(viewValue.replace(',', '.')); + return parseFloat(viewValue.replace('.', '').replace(',', '.')); + } else if (FLOAT_REGEXP_2.test(Number(viewValue))) { + ctrl.$setValidity('float', true); + return parseFloat(viewValue.replace(',', '')); } else { ctrl.$setValidity('float', false); return undefined; @@ -211,6 +216,21 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job }); } }; + // var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/; + // return { + // require: 'ngModel', + // link: function(scope, elm, attrs, ctrl) { + // ctrl.$parsers.unshift(function(viewValue) { + // if (FLOAT_REGEXP.test(viewValue)) { + // ctrl.$setValidity('float', true); + // return parseFloat(viewValue.replace(',', '.')); + // } else { + // ctrl.$setValidity('float', false); + // return undefined; + // } + // }); + // } + // }; }) // integer Validate that input is of type integer. Taken from Angular developer @@ -221,6 +241,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job // override/interfere with this directive. .directive('integer', function() { return { + restrict: 'A', require: 'ngModel', link: function(scope, elm, attrs, ctrl) { ctrl.$parsers.unshift(function(viewValue) { From 7e109cb95d7d41cf079c0fbe2fdac938d904becc Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 25 Nov 2014 15:28:52 -0500 Subject: [PATCH 03/15] Track source_script in summary and related fields for relevant inventory sources --- awx/api/serializers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 3369d51dfc..358a8b30d6 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -81,6 +81,7 @@ SUMMARIZABLE_FK_FIELDS = { 'current_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'), 'current_job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'), 'inventory_source': ('source', 'last_updated', 'status'), + 'source_script': ('name', 'description'), } class ChoiceField(fields.ChoiceField): @@ -1037,6 +1038,8 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) if obj.group and obj.group.active: res['group'] = reverse('api:group_detail', args=(obj.group.pk,)) + if obj.source_script: + res['source_script'] = reverse('api:inventory_script_detail', args=(obj.source_script.pk,)) # Backwards compatibility. if obj.current_update: res['current_update'] = reverse('api:inventory_update_detail', From 08ea3bef25bbe12614861218ad8c090b169cc1b3 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 25 Nov 2014 16:22:48 -0500 Subject: [PATCH 04/15] Add system job template launch documentation --- awx/api/templates/api/system_job_template_launch.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 awx/api/templates/api/system_job_template_launch.md diff --git a/awx/api/templates/api/system_job_template_launch.md b/awx/api/templates/api/system_job_template_launch.md new file mode 100644 index 0000000000..228b8887d0 --- /dev/null +++ b/awx/api/templates/api/system_job_template_launch.md @@ -0,0 +1,10 @@ +Launch a Job Template: + +Make a POST request to this resource to launch the system job template. + +An extra parameter `extra_vars` is suggested in order to pass extra parameters +to the system job task. For example: `{"days": 30}` to perform the action on +items older than 30 days. + +If successful, the response status code will be 202. If the job cannot be +launched, a 405 status code will be returned. From 8718688cf828882f70ebff28150967e1a5f06d86 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 25 Nov 2014 16:47:53 -0500 Subject: [PATCH 05/15] Make sure we pass extra parameters down to the unified job create method so we can pick up credential if given --- 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 196a2807ba..9247cfb149 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1499,7 +1499,7 @@ class JobTemplateLaunch(GenericAPIView): if validation_errors: return Response(dict(errors=validation_errors), status=status.HTTP_400_BAD_REQUEST) - new_job = obj.create_unified_job() + new_job = obj.create_unified_job(**request.DATA) result = new_job.signal_start(**request.DATA) if not result: data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) From 9756487edf856933c8ab99b52df9cfe9be8abe2f Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 25 Nov 2014 16:57:17 -0500 Subject: [PATCH 06/15] Make sure when we call create_unified_job we can pass important fields as _id or without --- awx/main/models/unified_jobs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index dd5ea06933..157b46a3bb 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -295,6 +295,10 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): kwargs.pop('%s_id' % parent_field_name, None) kwargs[parent_field_name] = self for field_name in self._get_unified_job_field_names(): + if hasattr(self, '%s_id' % field_name) and field_name in kwargs: + kwargs['%s_id' % field_name] = kwargs[field_name] + kwargs.pop(field_name) + continue if field_name in kwargs: continue # Foreign keys can be specified as field_name or field_name_id. From 35c392ab17795b9ee797c15426867bba3199a2cc Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 26 Nov 2014 03:20:37 -0500 Subject: [PATCH 07/15] Add instance filters and group by options for EC2 inventory sources. Implements https://trello.com/c/QOVhP0mH --- awx/api/serializers.py | 6 +- awx/main/migrations/0061_v210_changes.py | 512 +++++++++++++++++++++++ awx/main/models/inventory.py | 63 ++- awx/main/tasks.py | 6 + awx/main/tests/inventory.py | 83 +++- awx/plugins/inventory/ec2.ini.example | 15 + awx/plugins/inventory/ec2.py | 191 ++++++--- awx/settings/defaults.py | 8 +- awx/ui/static/js/forms/Source.js | 39 +- awx/ui/static/js/helpers/Groups.js | 61 ++- 10 files changed, 869 insertions(+), 115 deletions(-) create mode 100644 awx/main/migrations/0061_v210_changes.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 358a8b30d6..3aea7ddc6e 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -960,7 +960,7 @@ class InventorySourceOptionsSerializer(BaseSerializer): class Meta: fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential', - 'source_regions', 'overwrite', 'overwrite_vars') + 'source_regions', 'instance_filters', 'group_by', 'overwrite', 'overwrite_vars') def get_related(self, obj): res = super(InventorySourceOptionsSerializer, self).get_related(obj) @@ -1000,6 +1000,10 @@ class InventorySourceOptionsSerializer(BaseSerializer): for cp in ('azure', 'ec2', 'gce', 'rax'): get_regions = getattr(self.opts.model, 'get_%s_region_choices' % cp) field_opts['%s_region_choices' % cp] = get_regions() + field_opts = metadata.get('group_by', {}) + for cp in ('ec2',): + get_group_by_choices = getattr(self.opts.model, 'get_%s_group_by_choices' % cp) + field_opts['%s_group_by_choices' % cp] = get_group_by_choices() return metadata def to_native(self, obj): diff --git a/awx/main/migrations/0061_v210_changes.py b/awx/main/migrations/0061_v210_changes.py new file mode 100644 index 0000000000..6eecea2146 --- /dev/null +++ b/awx/main/migrations/0061_v210_changes.py @@ -0,0 +1,512 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'InventorySource.instance_filters' + db.add_column(u'main_inventorysource', 'instance_filters', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + + # Adding field 'InventorySource.group_by' + db.add_column(u'main_inventorysource', 'group_by', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + + # Adding field 'InventoryUpdate.instance_filters' + db.add_column(u'main_inventoryupdate', 'instance_filters', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + + # Adding field 'InventoryUpdate.group_by' + db.add_column(u'main_inventoryupdate', 'group_by', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'InventorySource.instance_filters' + db.delete_column(u'main_inventorysource', 'instance_filters') + + # Deleting field 'InventorySource.group_by' + db.delete_column(u'main_inventorysource', 'group_by') + + # Deleting field 'InventoryUpdate.instance_filters' + db.delete_column(u'main_inventoryupdate', 'instance_filters') + + # Deleting field 'InventoryUpdate.group_by' + db.delete_column(u'main_inventoryupdate', 'group_by') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.activitystream': { + 'Meta': {'object_name': 'ActivityStream'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_stream'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'changes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'credential': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Credential']", 'symmetrical': 'False', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'host': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Host']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Inventory']", 'symmetrical': 'False', 'blank': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventorySource']", 'symmetrical': 'False', 'blank': 'True'}), + 'inventory_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventoryUpdate']", 'symmetrical': 'False', 'blank': 'True'}), + 'job': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Job']", 'symmetrical': 'False', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.JobTemplate']", 'symmetrical': 'False', 'blank': 'True'}), + 'object1': ('django.db.models.fields.TextField', [], {}), + 'object2': ('django.db.models.fields.TextField', [], {}), + 'object_relationship_type': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'operation': ('django.db.models.fields.CharField', [], {'max_length': '13'}), + 'organization': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Organization']", 'symmetrical': 'False', 'blank': 'True'}), + 'permission': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Project']", 'symmetrical': 'False', 'blank': 'True'}), + 'project_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.ProjectUpdate']", 'symmetrical': 'False', 'blank': 'True'}), + 'schedule': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Schedule']", 'symmetrical': 'False', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Team']", 'symmetrical': 'False', 'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'unified_job': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job+'", 'blank': 'True', 'to': "orm['main.UnifiedJob']"}), + 'unified_job_template': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job_template+'", 'blank': 'True', 'to': "orm['main.UnifiedJobTemplate']"}), + 'user': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'main.authtoken': { + 'Meta': {'object_name': 'AuthToken'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'request_hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': u"orm['auth.User']"}) + }, + 'main.credential': { + 'Meta': {'ordering': "('kind', 'name')", 'unique_together': "[('user', 'team', 'kind', 'name')]", 'object_name': 'Credential'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cloud': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'kind': ('django.db.models.fields.CharField', [], {'default': "'ssh'", 'max_length': '32'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'ssh_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'su_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'su_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': u"orm['auth.User']"}), + 'username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'vault_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}) + }, + 'main.custominventoryscript': { + 'Meta': {'ordering': "('name',)", 'object_name': 'CustomInventoryScript'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'custominventoryscript\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'custominventoryscript\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'script': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.group': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('name', 'inventory'),)", 'object_name': 'Group'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.Host']"}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'groups'", 'symmetrical': 'False', 'to': "orm['main.InventorySource']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'children'", 'blank': 'True', 'to': "orm['main.Group']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.host': { + 'Meta': {'ordering': "('inventory', 'name')", 'unique_together': "(('name', 'inventory'),)", 'object_name': 'Host'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'hosts'", 'symmetrical': 'False', 'to': "orm['main.InventorySource']"}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'hosts_as_last_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Job']"}), + 'last_job_host_summary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job_summary+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobHostSummary']", 'blank': 'True', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.instance': { + 'Meta': {'object_name': 'Instance'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}) + }, + 'main.inventory': { + 'Meta': {'ordering': "('name',)", 'unique_together': "[('name', 'organization')]", 'object_name': 'Inventory'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory_sources_with_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventories'", 'to': "orm['main.Organization']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_inventory_sources': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.inventorysource': { + 'Meta': {'object_name': 'InventorySource', '_ormbases': ['main.UnifiedJobTemplate']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventorysources'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'group': ('awx.main.fields.AutoOneToOneField', [], {'default': 'None', 'related_name': "'inventory_source'", 'unique': 'True', 'null': 'True', 'to': "orm['main.Group']"}), + 'group_by': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'instance_filters': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_sources'", 'null': 'True', 'to': "orm['main.Inventory']"}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_script': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.CustomInventoryScript']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}), + 'update_cache_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'main.inventoryupdate': { + 'Meta': {'object_name': 'InventoryUpdate', '_ormbases': ['main.UnifiedJob']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventoryupdates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'group_by': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'instance_filters': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventory_updates'", 'to': "orm['main.InventorySource']"}), + 'license_error': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_script': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.CustomInventoryScript']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.job': { + 'Meta': {'ordering': "('id',)", 'object_name': 'Job', '_ormbases': ['main.UnifiedJob']}, + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'force_handlers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'jobs'", 'symmetrical': 'False', 'through': "orm['main.JobHostSummary']", 'to': "orm['main.Host']"}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobTemplate']", 'blank': 'True', 'null': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'skip_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'start_at_task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.jobevent': { + 'Meta': {'ordering': "('pk',)", 'object_name': 'JobEvent'}, + 'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'event_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'job_events_as_primary_host'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}), + 'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'job_events'", 'symmetrical': 'False', 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.JobEvent']"}), + 'play': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'role': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}) + }, + 'main.jobhostsummary': { + 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('job', 'host_name')]", 'object_name': 'JobHostSummary'}, + 'changed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'dark': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'job_host_summaries'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}), + 'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'ok': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'processed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'skipped': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'main.joborigin': { + 'Meta': {'object_name': 'JobOrigin'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Instance']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'unified_job': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'job_origin'", 'unique': 'True', 'to': "orm['main.UnifiedJob']"}) + }, + 'main.jobtemplate': { + 'Meta': {'ordering': "('name',)", 'object_name': 'JobTemplate', '_ormbases': ['main.UnifiedJobTemplate']}, + 'ask_variables_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'force_handlers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'host_config_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'skip_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'start_at_task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'survey_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'survey_spec': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.organization': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Organization'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.permission': { + 'Meta': {'object_name': 'Permission'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}) + }, + 'main.profile': { + 'Meta': {'object_name': 'Profile'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ldap_dn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.project': { + 'Meta': {'ordering': "('id',)", 'object_name': 'Project', '_ormbases': ['main.UnifiedJobTemplate']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_next_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}), + 'scm_update_cache_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'scm_update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.projectupdate': { + 'Meta': {'object_name': 'ProjectUpdate', '_ormbases': ['main.UnifiedJob']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projectupdates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_updates'", 'to': "orm['main.Project']"}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.schedule': { + 'Meta': {'ordering': "['-next_run']", 'object_name': 'Schedule'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'schedule\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'dtend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'dtstart': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'extra_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'schedule\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'next_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'rrule': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'unified_job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'schedules'", 'to': "orm['main.UnifiedJobTemplate']"}) + }, + 'main.systemjob': { + 'Meta': {'ordering': "('id',)", 'object_name': 'SystemJob', '_ormbases': ['main.UnifiedJob']}, + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'system_job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.SystemJobTemplate']", 'blank': 'True', 'null': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.systemjobtemplate': { + 'Meta': {'object_name': 'SystemJobTemplate', '_ormbases': ['main.UnifiedJobTemplate']}, + 'job_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.team': { + 'Meta': {'ordering': "('organization__name', 'name')", 'unique_together': "[('organization', 'name')]", 'object_name': 'Team'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'teams'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.unifiedjob': { + 'Meta': {'object_name': 'UnifiedJob'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjob\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'dependent_jobs': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'dependent_jobs_rel_+'", 'to': "orm['main.UnifiedJob']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'elapsed': ('django.db.models.fields.DecimalField', [], {'max_digits': '12', 'decimal_places': '3'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'finished': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'job_explanation': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'launch_type': ('django.db.models.fields.CharField', [], {'default': "'manual'", 'max_length': '20'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjob\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'old_pk': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'null': 'True'}), + 'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'polymorphic_main.unifiedjob_set'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}), + 'result_stdout_file': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_stdout_text': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'schedule': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.Schedule']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'start_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'started': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}), + 'unified_job_template': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjob_unified_jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJobTemplate']"}) + }, + 'main.unifiedjobtemplate': { + 'Meta': {'unique_together': "[('polymorphic_ctype', 'name')]", 'object_name': 'UnifiedJobTemplate'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjobtemplate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'current_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_current_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJob']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'has_schedules': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_last_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJob']"}), + 'last_job_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_job_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjobtemplate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'next_job_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'next_schedule': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_next_schedule+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Schedule']"}), + 'old_pk': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'null': 'True'}), + 'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'polymorphic_main.unifiedjobtemplate_set'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'ok'", 'max_length': '32'}) + } + } + + complete_apps = ['main'] \ No newline at end of file diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 928972dad1..38c8247b07 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -784,6 +784,18 @@ class InventorySourceOptions(BaseModel): blank=True, default='', ) + instance_filters = models.CharField( + max_length=1024, + blank=True, + default='', + help_text=_('Comma-separated list of filter expressions (EC2 only). Hosts are imported when ANY of the filters match.'), + ) + group_by = models.CharField( + max_length=1024, + blank=True, + default='', + help_text=_('Limit groups automatically created from inventory source (EC2 only).'), + ) overwrite = models.BooleanField( default=False, help_text=_('Overwrite local groups and hosts from remote inventory source.'), @@ -815,6 +827,20 @@ class InventorySourceOptions(BaseModel): regions.append((region.name, label)) return regions + @classmethod + def get_ec2_group_by_choices(cls): + return [ + ('availability_zone', 'Availability Zone'), + ('ami_id', 'Image ID'), + ('instance_id', 'Instance ID'), + ('instance_type', 'Instance Type'), + ('key_pair', 'Key Name'), + ('region', 'Region'), + ('security_group', 'Security Group'), + ('tag_keys', 'Tags'), + ('vpc_id', 'VPC ID'), + ] + @classmethod def get_rax_region_choices(cls): # Not possible to get rax regions without first authenticating, so use @@ -900,6 +926,41 @@ class InventorySourceOptions(BaseModel): source_vars_dict = VarsDictProperty('source_vars') + def clean_instance_filters(self): + if self.source != 'ec2': + return '' + invalid_filters = [] + instance_filter_re = re.compile(r'^(?:tag:.+)|(?:[a-z][a-z\.-]*[a-z])=.*$') + for instance_filter in self.instance_filters.split(','): + instance_filter = instance_filter.strip() + if not instance_filter: + continue + if not instance_filter_re.match(instance_filter): + invalid_filters.append(instance_filter) + if invalid_filters: + raise ValidationError('Invalid filter expression%s: %s' % + ('' if len(invalid_filters) == 1 else 's', + ', '.join(invalid_filters))) + return self.instance_filters + + def clean_group_by(self): + if self.source != 'ec2': + return '' + get_choices = getattr(self, 'get_%s_group_by_choices' % self.source) + valid_choices = [x[0] for x in get_choices()] + choice_transform = lambda x: x.strip().lower() + valid_choices = [choice_transform(x) for x in valid_choices] + choices = [choice_transform(x) for x in self.group_by.split(',') if x.strip()] + invalid_choices = [] + for c in choices: + if c not in valid_choices and c not in invalid_choices: + invalid_choices.append(c) + if invalid_choices: + raise ValidationError('Invalid group by choice%s: %s' % + ('' if len(invalid_choices) == 1 else 's', + ', '.join(invalid_choices))) + return ','.join(choices) + class InventorySource(UnifiedJobTemplate, InventorySourceOptions): @@ -936,7 +997,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions): @classmethod def _get_unified_job_field_names(cls): return ['name', 'description', 'source', 'source_path', 'source_script', 'source_vars', - 'credential', 'source_regions', 'overwrite', 'overwrite_vars'] + 'credential', 'source_regions', 'instance_filters', 'group_by', 'overwrite', 'overwrite_vars'] def save(self, *args, **kwargs): new_instance = bool(self.pk) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index fff1209593..d5f77f1145 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -956,6 +956,12 @@ class RunInventoryUpdate(BaseTask): ec2_opts.setdefault('all_rds_instances', 'False') ec2_opts.setdefault('rds', 'False') ec2_opts.setdefault('nested_groups', 'True') + if inventory_update.instance_filters: + ec2_opts.setdefault('instance_filters', inventory_update.instance_filters) + group_by = [x.strip().lower() for x in inventory_update.group_by.split(',') if x.strip()] + for choice in inventory_update.get_ec2_group_by_choices(): + value = bool((group_by and choice[0] in group_by) or (not group_by and choice[0] != 'instance_id')) + ec2_opts.setdefault('group_by_%s' % choice[0], str(value)) if 'cache_path' not in ec2_opts: cache_path = tempfile.mkdtemp(prefix='ec2_cache', dir=kwargs.get('private_data_dir', None)) ec2_opts['cache_path'] = cache_path diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index 0a16cf431e..1d9cb517e1 100644 --- a/awx/main/tests/inventory.py +++ b/awx/main/tests/inventory.py @@ -1135,7 +1135,7 @@ class InventoryUpdatesTest(BaseTransactionTest): pass # If should_fail is None, we don't care. return inventory_update - def check_inventory_source(self, inventory_source, initial=True, enabled_host_pks=None): + def check_inventory_source(self, inventory_source, initial=True, enabled_host_pks=None, instance_id_group_ok=False): enabled_host_pks = enabled_host_pks or set() inventory_source = InventorySource.objects.get(pk=inventory_source.pk) inventory = inventory_source.group.inventory @@ -1158,7 +1158,7 @@ class InventoryUpdatesTest(BaseTransactionTest): url = reverse('api:inventory_source_hosts_list', args=(inventory_source.pk,)) response = self.get(url, expect=200) self.assertNotEqual(response['count'], 0) - for host in inventory.hosts.all(): + for host in inventory.hosts.filter(active=True): source_pks = host.inventory_sources.values_list('pk', flat=True) self.assertTrue(inventory_source.pk in source_pks) self.assertTrue(host.has_inventory_sources) @@ -1172,20 +1172,21 @@ class InventoryUpdatesTest(BaseTransactionTest): url = reverse('api:host_inventory_sources_list', args=(host.pk,)) response = self.get(url, expect=200) self.assertNotEqual(response['count'], 0) - for group in inventory.groups.all(): + for group in inventory.groups.filter(active=True): source_pks = group.inventory_sources.values_list('pk', flat=True) self.assertTrue(inventory_source.pk in source_pks) self.assertTrue(group.has_inventory_sources) self.assertTrue(group.children.filter(active=True).exists() or group.hosts.filter(active=True).exists()) # Make sure EC2 instance ID groups and RDS groups are excluded. - if inventory_source.source == 'ec2': + if inventory_source.source == 'ec2' and not instance_id_group_ok: self.assertFalse(re.match(r'^i-[0-9a-f]{8}$', group.name, re.I), group.name) + if inventory_source.source == 'ec2': self.assertFalse(re.match(r'^rds|rds_.+|type_db_.+$', group.name, re.I), group.name) # Make sure Rackspace instance ID groups are excluded. - if inventory_source.source == 'rax': + if inventory_source.source == 'rax' and not instance_id_group_ok: self.assertFalse(re.match(r'^instance-.+$', group.name, re.I), group.name) with self.current_user(self.super_django_user): @@ -1194,7 +1195,7 @@ class InventoryUpdatesTest(BaseTransactionTest): self.assertNotEqual(response['count'], 0) # Try to set a source on a child group that was imported. Should not # be allowed. - for group in inventory_source.group.children.all(): + for group in inventory_source.group.children.filter(active=True): inv_src_2 = group.inventory_source inv_src_url2 = reverse('api:inventory_source_detail', args=(inv_src_2.pk,)) with self.current_user(self.super_django_user): @@ -1450,10 +1451,15 @@ class InventoryUpdatesTest(BaseTransactionTest): # Also change the host name, and verify it is not deleted, but instead # updated because the instance ID matches. enabled_host_pks = set(self.inventory.hosts.filter(enabled=True).values_list('pk', flat=True)) + instance_types = {} for host in self.inventory.hosts.all(): host.enabled = False host.name = 'changed-%s' % host.name host.save() + # Get instance types for later use with instance_filters. + instance_type = host.variables_dict.get('ec2_instance_type', '') + if instance_type: + instance_types.setdefault(instance_type, []).append(host.pk) old_host_pks = set(self.inventory.hosts.values_list('pk', flat=True)) self.check_inventory_source(inventory_source, initial=False, enabled_host_pks=enabled_host_pks) new_host_pks = set(self.inventory.hosts.values_list('pk', flat=True)) @@ -1461,6 +1467,18 @@ class InventoryUpdatesTest(BaseTransactionTest): # Verify that main group is in top level groups (hasn't been added as # its own child). self.assertTrue(self.group in self.inventory.root_groups) + # Now add instance filters and verify that only the matching hosts are + # synced, specify new cache path to force refresh. + cache_path2 = tempfile.mkdtemp(prefix='awx_ec2_') + self._temp_paths.append(cache_path2) + instance_type = max(instance_types.items(), key=lambda x: len(x[1]))[0] + inventory_source.instance_filters = 'instance-type=%s' % instance_type + inventory_source.source_vars = '---\n\nnested_groups: false\ncache_path: %s\n' % cache_path2 + inventory_source.overwrite = True + inventory_source.save() + self.check_inventory_source(inventory_source, initial=False) + new_host_pks = set(self.inventory.hosts.filter(active=True).values_list('pk', flat=True)) + self.assertEqual(new_host_pks, set(instance_types[instance_type])) def test_update_from_ec2_with_nested_groups(self): source_username = getattr(settings, 'TEST_AWS_ACCESS_KEY_ID', '') @@ -1494,16 +1512,67 @@ class InventoryUpdatesTest(BaseTransactionTest): self.assertFalse(name.startswith('key_')) self.assertFalse(name.startswith('security_group_')) self.assertFalse(name.startswith('tag_')) + self.assertFalse(name.startswith('ami-')) + self.assertFalse(name.startswith('vpc-')) self.assertTrue('ec2' in child_names) self.assertTrue('regions' in child_names) self.assertTrue('types' in child_names) self.assertTrue('keys' in child_names) self.assertTrue('security_groups' in child_names) self.assertTrue('tags' in child_names) + self.assertTrue('images' in child_names) + self.assertFalse('instances' in child_names) # Make sure we clean up the cache path when finished (when one is not # provided explicitly via source_vars). new_cache_paths = set(glob.glob(cache_path_pattern)) self.assertEqual(old_cache_paths, new_cache_paths) + # Sync again with group_by set to a non-empty value. + cache_path = tempfile.mkdtemp(prefix='awx_ec2_') + self._temp_paths.append(cache_path) + inventory_source.group_by = 'region,instance_type' + inventory_source.source_vars = '---\n\ncache_path: %s\n' % cache_path + inventory_source.overwrite = True + inventory_source.save() + self.check_inventory_source(inventory_source, initial=False) + # Verify that only the desired groups are returned. + child_names = self.group.children.filter(active=True).values_list('name', flat=True) + self.assertTrue('ec2' in child_names) + self.assertTrue('regions' in child_names) + self.assertTrue(self.group.children.get(name='regions').children.filter(active=True).count()) + self.assertTrue('types' in child_names) + self.assertTrue(self.group.children.get(name='types').children.filter(active=True).count()) + self.assertFalse('keys' in child_names) + self.assertFalse('security_groups' in child_names) + self.assertFalse('tags' in child_names) + self.assertFalse('images' in child_names) + self.assertFalse('vpcs' in child_names) + self.assertFalse('instances' in child_names) + # Sync again with group_by set to include all possible groups. + cache_path2 = tempfile.mkdtemp(prefix='awx_ec2_') + self._temp_paths.append(cache_path2) + inventory_source.group_by = 'instance_id, region, availability_zone, ami_id, instance_type, key_pair, vpc_id, security_group, tag_keys' + inventory_source.source_vars = '---\n\ncache_path: %s\n' % cache_path2 + inventory_source.overwrite = True + inventory_source.save() + self.check_inventory_source(inventory_source, initial=False, instance_id_group_ok=True) + # Verify that only the desired groups are returned. + # Skip vpcs as selected inventory may or may not have any. + child_names = self.group.children.filter(active=True).values_list('name', flat=True) + self.assertTrue('ec2' in child_names) + self.assertTrue('regions' in child_names) + self.assertTrue(self.group.children.get(name='regions').children.filter(active=True).count()) + self.assertTrue('types' in child_names) + self.assertTrue(self.group.children.get(name='types').children.filter(active=True).count()) + self.assertTrue('keys' in child_names) + self.assertTrue(self.group.children.get(name='keys').children.filter(active=True).count()) + self.assertTrue('security_groups' in child_names) + self.assertTrue(self.group.children.get(name='security_groups').children.filter(active=True).count()) + self.assertTrue('tags' in child_names) + self.assertTrue(self.group.children.get(name='tags').children.filter(active=True).count()) + self.assertTrue('images' in child_names) + self.assertTrue(self.group.children.get(name='images').children.filter(active=True).count()) + self.assertTrue('instances' in child_names) + self.assertTrue(self.group.children.get(name='instances').children.filter(active=True).count()) return # Print out group/host tree for debugging. print @@ -1515,7 +1584,7 @@ class InventoryUpdatesTest(BaseTransactionTest): draw_tree(c, d+1) for g in self.inventory.root_groups.order_by('name'): draw_tree(g) - + def test_update_from_rax(self): source_username = getattr(settings, 'TEST_RACKSPACE_USERNAME', '') source_password = getattr(settings, 'TEST_RACKSPACE_API_KEY', '') diff --git a/awx/plugins/inventory/ec2.ini.example b/awx/plugins/inventory/ec2.ini.example index c66bf309b1..adc7d13513 100644 --- a/awx/plugins/inventory/ec2.ini.example +++ b/awx/plugins/inventory/ec2.ini.example @@ -68,6 +68,21 @@ cache_max_age = 300 # Organize groups into a nested/hierarchy instead of a flat namespace. nested_groups = False +# The EC2 inventory output can become very large. To manage its size, +# configure which groups should be created. +group_by_instance_id = True +group_by_region = True +group_by_availability_zone = True +group_by_ami_id = True +group_by_instance_type = True +group_by_key_pair = True +group_by_vpc_id = True +group_by_security_group = True +group_by_tag_keys = True +group_by_route53_names = True +group_by_rds_engine = True +group_by_rds_parameter_group = True + # If you only want to include hosts that match a certain regular expression # pattern_include = stage-* diff --git a/awx/plugins/inventory/ec2.py b/awx/plugins/inventory/ec2.py index 9d2dec38d3..3d88cbf450 100755 --- a/awx/plugins/inventory/ec2.py +++ b/awx/plugins/inventory/ec2.py @@ -253,6 +253,27 @@ class Ec2Inventory(object): else: self.nested_groups = False + # Configure which groups should be created. + group_by_options = [ + 'group_by_instance_id', + 'group_by_region', + 'group_by_availability_zone', + 'group_by_ami_id', + 'group_by_instance_type', + 'group_by_key_pair', + 'group_by_vpc_id', + 'group_by_security_group', + 'group_by_tag_keys', + 'group_by_route53_names', + 'group_by_rds_engine', + 'group_by_rds_parameter_group', + ] + for option in group_by_options: + if config.has_option('ec2', option): + setattr(self, option, config.getboolean('ec2', option)) + else: + setattr(self, option, True) + # Do we need to just include hosts that match a pattern? try: pattern_include = config.get('ec2', 'pattern_include') @@ -405,56 +426,77 @@ class Ec2Inventory(object): self.index[dest] = [region, instance.id] # Inventory: Group by instance ID (always a group of 1) - self.inventory[instance.id] = [dest] - if self.nested_groups: - self.push_group(self.inventory, 'instances', instance.id) + if self.group_by_instance_id: + self.inventory[instance.id] = [dest] + if self.nested_groups: + self.push_group(self.inventory, 'instances', instance.id) # Inventory: Group by region - if self.nested_groups: - self.push_group(self.inventory, 'regions', region) - else: + if self.group_by_region: self.push(self.inventory, region, dest) + if self.nested_groups: + self.push_group(self.inventory, 'regions', region) # Inventory: Group by availability zone - self.push(self.inventory, instance.placement, dest) - if self.nested_groups: - self.push_group(self.inventory, region, instance.placement) + if self.group_by_availability_zone: + self.push(self.inventory, instance.placement, dest) + if self.nested_groups: + if self.group_by_region: + self.push_group(self.inventory, region, instance.placement) + self.push_group(self.inventory, 'zones', instance.placement) + + # Inventory: Group by Amazon Machine Image (AMI) ID + if self.group_by_ami_id: + ami_id = self.to_safe(instance.image_id) + self.push(self.inventory, ami_id, dest) + if self.nested_groups: + self.push_group(self.inventory, 'images', ami_id) # Inventory: Group by instance type - type_name = self.to_safe('type_' + instance.instance_type) - self.push(self.inventory, type_name, dest) - if self.nested_groups: - self.push_group(self.inventory, 'types', type_name) + if self.group_by_instance_type: + type_name = self.to_safe('type_' + instance.instance_type) + self.push(self.inventory, type_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'types', type_name) # Inventory: Group by key pair - if instance.key_name: + if self.group_by_key_pair and instance.key_name: key_name = self.to_safe('key_' + instance.key_name) self.push(self.inventory, key_name, dest) if self.nested_groups: self.push_group(self.inventory, 'keys', key_name) - + + # Inventory: Group by VPC + if self.group_by_vpc_id and instance.vpc_id: + vpc_id_name = self.to_safe(instance.vpc_id) + self.push(self.inventory, vpc_id_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'vpcs', vpc_id_name) + # Inventory: Group by security group - try: - for group in instance.groups: - key = self.to_safe("security_group_" + group.name) - self.push(self.inventory, key, dest) - if self.nested_groups: - self.push_group(self.inventory, 'security_groups', key) - except AttributeError: - print 'Package boto seems a bit older.' - print 'Please upgrade boto >= 2.3.0.' - sys.exit(1) + if self.group_by_security_group: + try: + for group in instance.groups: + key = self.to_safe("security_group_" + group.name) + self.push(self.inventory, key, dest) + if self.nested_groups: + self.push_group(self.inventory, 'security_groups', key) + except AttributeError: + print 'Package boto seems a bit older.' + print 'Please upgrade boto >= 2.3.0.' + sys.exit(1) # Inventory: Group by tag keys - for k, v in instance.tags.iteritems(): - key = self.to_safe("tag_" + k + "=" + v) - self.push(self.inventory, key, dest) - if self.nested_groups: - self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k)) - self.push_group(self.inventory, self.to_safe("tag_" + k), key) + if self.group_by_tag_keys: + for k, v in instance.tags.iteritems(): + key = self.to_safe("tag_" + k + "=" + v) + self.push(self.inventory, key, dest) + if self.nested_groups: + self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k)) + self.push_group(self.inventory, self.to_safe("tag_" + k), key) # Inventory: Group by Route53 domain names if enabled - if self.route53_enabled: + if self.route53_enabled and self.group_by_route53_names: route53_names = self.get_instance_route53_names(instance) for name in route53_names: self.push(self.inventory, name, dest) @@ -476,10 +518,6 @@ class Ec2Inventory(object): return # Select the best destination address - #if instance.subnet_id: - #dest = getattr(instance, self.vpc_destination_variable) - #else: - #dest = getattr(instance, self.destination_variable) dest = instance.endpoint[0] if not dest: @@ -490,49 +528,64 @@ class Ec2Inventory(object): self.index[dest] = [region, instance.id] # Inventory: Group by instance ID (always a group of 1) - self.inventory[instance.id] = [dest] - if self.nested_groups: - self.push_group(self.inventory, 'instances', instance.id) + if self.group_by_instance_id: + self.inventory[instance.id] = [dest] + if self.nested_groups: + self.push_group(self.inventory, 'instances', instance.id) # Inventory: Group by region - if self.nested_groups: - self.push_group(self.inventory, 'regions', region) - else: + if self.group_by_region: self.push(self.inventory, region, dest) + if self.nested_groups: + self.push_group(self.inventory, 'regions', region) # Inventory: Group by availability zone - self.push(self.inventory, instance.availability_zone, dest) - if self.nested_groups: - self.push_group(self.inventory, region, instance.availability_zone) - - # Inventory: Group by instance type - type_name = self.to_safe('type_' + instance.instance_class) - self.push(self.inventory, type_name, dest) - if self.nested_groups: - self.push_group(self.inventory, 'types', type_name) - - # Inventory: Group by security group - try: - if instance.security_group: - key = self.to_safe("security_group_" + instance.security_group.name) - self.push(self.inventory, key, dest) - if self.nested_groups: - self.push_group(self.inventory, 'security_groups', key) + if self.group_by_availability_zone: + self.push(self.inventory, instance.availability_zone, dest) + if self.nested_groups: + if self.group_by_region: + self.push_group(self.inventory, region, instance.availability_zone) + self.push_group(self.inventory, 'zones', instance.availability_zone) - except AttributeError: - print 'Package boto seems a bit older.' - print 'Please upgrade boto >= 2.3.0.' - sys.exit(1) + # Inventory: Group by instance type + if self.group_by_instance_type: + type_name = self.to_safe('type_' + instance.instance_class) + self.push(self.inventory, type_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'types', type_name) + + # Inventory: Group by VPC + if self.group_by_vpc_id and instance.vpc_id: + vpc_id_name = self.to_safe(instance.vpc_id) + self.push(self.inventory, vpc_id_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'vpcs', vpc_id_name) + + # Inventory: Group by security group + if self.group_by_security_group: + try: + if instance.security_group: + key = self.to_safe("security_group_" + instance.security_group.name) + self.push(self.inventory, key, dest) + if self.nested_groups: + self.push_group(self.inventory, 'security_groups', key) + + except AttributeError: + print 'Package boto seems a bit older.' + print 'Please upgrade boto >= 2.3.0.' + sys.exit(1) # Inventory: Group by engine - self.push(self.inventory, self.to_safe("rds_" + instance.engine), dest) - if self.nested_groups: - self.push_group(self.inventory, 'rds_engines', self.to_safe("rds_" + instance.engine)) + if self.group_by_rds_engine: + self.push(self.inventory, self.to_safe("rds_" + instance.engine), dest) + if self.nested_groups: + self.push_group(self.inventory, 'rds_engines', self.to_safe("rds_" + instance.engine)) # Inventory: Group by parameter group - self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), dest) - if self.nested_groups: - self.push_group(self.inventory, 'rds_parameter_groups', self.to_safe("rds_parameter_group_" + instance.parameter_group.name)) + if self.group_by_rds_parameter_group: + self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), dest) + if self.nested_groups: + self.push_group(self.inventory, 'rds_parameter_groups', self.to_safe("rds_parameter_group_" + instance.parameter_group.name)) # Global Tag: all RDS instances self.push(self.inventory, 'rds', dest) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 6e3bb1c4ea..90ed8390ef 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -387,15 +387,13 @@ EC2_ENABLED_VALUE = 'running' EC2_INSTANCE_ID_VAR = 'ec2_id' # Filter for allowed group/host names when importing inventory from EC2. -# By default, filter group of one created for each instance, filter all RDS -# hosts, and exclude all groups without children, hosts and variables. -EC2_GROUP_FILTER = r'^(?!i-[a-f0-9]{8,}).+$' +EC2_GROUP_FILTER = r'^.+$' EC2_HOST_FILTER = r'^.+$' EC2_EXCLUDE_EMPTY_GROUPS = True # ------------ -# -- VMWare -- +# -- VMware -- # ------------ VMWARE_REGIONS_BLACKLIST = [] @@ -408,7 +406,7 @@ VMWARE_ENABLED_VALUE = 'poweredOn' VMWARE_INSTANCE_ID_VAR = 'vmware_uuid' # Filter for allowed group and host names when importing inventory -# from EC2. +# from VMware. VMWARE_GROUP_FILTER = r'^.+$' VMWARE_HOST_FILTER = r'^.+$' VMWARE_EXCLUDE_EMPTY_GROUPS = True diff --git a/awx/ui/static/js/forms/Source.js b/awx/ui/static/js/forms/Source.js index e65ad00dff..9fc9db75fd 100644 --- a/awx/ui/static/js/forms/Source.js +++ b/awx/ui/static/js/forms/Source.js @@ -71,7 +71,16 @@ angular.module('SourceFormDefinition', []) editRequired: false, dataTitle: 'Instance Filters', dataPlacement: 'right', - awPopOver: "

Open the documentation for a complete list of filter options.

", + awPopOver: "

Provide a comma-separated list of filter expressions. " + + "Hosts are imported to Tower when ANY of the filters match.

" + + "Limit to hosts having a tag:
\n" + + "
tag-key=TowerManaged
\n" + + "Limit to hosts using either key pair:
\n" + + "
key-name=staging, key-name=production
\n" + + "Limit to hosts where the Name tag begins with test:
\n" + + "
tag:Name=test*
\n" + + "

View the DescribeInstances documentation " + + "for a complete list of supported filters.

", dataContainer: 'body' }, group_by: { @@ -81,23 +90,23 @@ angular.module('SourceFormDefinition', []) addRequired: false, editRequired: false, awMultiselect: 'group_by_choices', - dataTitle: 'Group By', + dataTitle: 'Only Group By', dataPlacement: 'right', - awPopOver: "

FIXME: Create these automatic groups by default. give examples

", + awPopOver: "

Select which groups to create automatically. " + + "Tower will create group names similar to the following examples based on the options selected:

    " + + "
  • Availability Zone: zones » us-east-1b
  • " + + "
  • Image ID: images » ami-b007ab1e
  • " + + "
  • Instance ID: instances » i-ca11ab1e
  • " + + "
  • Instance Type: types » type_m1_medium
  • " + + "
  • Key Name: keys » key_testing
  • " + + "
  • Region: regions » us-east-1
  • " + + "
  • Security Group: security_groups » security_group_default
  • " + + "
  • Tags: tags » tag_Name » tag_Name_host1
  • " + + "
  • VPC ID: vpcs » vpc-5ca1ab1e
  • " + + "

If blank, all groups above are created except Instance ID.

", 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' - // }, - source_script: { + custom_script: { label : "Custom Inventory Scripts", type: 'lookup', ngShow: "source && source.value !== '' && source.value === 'custom'", diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 091b12e00c..9410b0027c 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -238,22 +238,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' id: 'all', text: 'All' }]); - // FIXME: Should come from API. - scope.group_by_choices = [ - {label: 'All', name: 'All', value: 'all'}, - {label: 'Instance ID', name: 'Instance ID', value: 'instance_id'}, - {label: 'Region', name: 'Region', value: 'region'}, - {label: 'Availability Zone', name: 'Availability Zone', value: 'availability_zone'}, - {label: 'AMI ID', name: 'AMI ID', value: 'ami_id'}, - {label: 'Instance Type', name: 'Instance Type', value: 'instance_type'}, - {label: 'Key Pair', name: 'Key Pair', value: 'key_pair'}, - {label: 'Security Group', name: 'Security Group', value: 'security_group'}, - {label: 'Tag Keys', name: 'Tag Keys', value: 'tag_keys'}, - ]; - $('#s2id_source_group_by').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; @@ -860,10 +846,13 @@ function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, Sched setTimeout(function(){ textareaResize('group_variables'); }, 300); } else if ($(e.target).text() === 'Source') { - if (sources_scope.source && (sources_scope.source.value === 'ec2' || sources_scope.source.value === 'custom')) { + if (sources_scope.source && (sources_scope.source.value === 'ec2')) { 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 === 'custom')) { + Wait('start'); ParseTypeChange({ scope: sources_scope, variable: 'extra_vars', parse_variable: SourceForm.fields.extra_vars.parseTypeName, field_id: 'source_extra_vars', onReady: waitStop }); } @@ -1009,6 +998,23 @@ function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, Sched }]; $('#s2id_source_source_regions').select2('data', master.source_regions); } + if (data.group_by && data.source === 'ec2') { + set = sources_scope.ec2_group_by; + opts = []; + list = data.group_by.split(','); + for (i = 0; i < list.length; i++) { + for (j = 0; j < set.length; j++) { + if (list[i] === set[j].value) { + opts.push({ + id: set[j].value, + text: set[j].label + }); + } + } + } + master.group_by = opts; + $('#s2id_source_group_by').select2('data', opts); + } sources_scope.group_update_url = data.related.update; modal_scope.$emit('groupVariablesLoaded'); // JT-- "groupVariablesLoaded" is where the schedule info is loaded, so I make a call after the sources_scope.source has been loaded //Wait('stop'); @@ -1119,6 +1125,16 @@ function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, Sched callback: 'choicesReadyGroup' }); + // Load options for group_by + GetChoices({ + scope: sources_scope, + url: GetBasePath('inventory_sources'), + field: 'group_by', + variable: 'ec2_group_by', + choice_name: 'ec2_group_by_choices', + callback: 'choicesReadyGroup' + }); + Wait('start'); if (parent_scope.removeAddTreeRefreshed) { @@ -1179,6 +1195,17 @@ function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, Sched data.source_regions = r.join(); if (sources_scope.source && (sources_scope.source.value === 'ec2')) { + data.instance_filters = sources_scope.instance_filters; + // Create a string out of selected list of regions + var group_by = $('#s2id_source_group_by').select2("data"); + r = []; + for (i = 0; i < group_by.length; i++) { + r.push(group_by[i].id); + } + data.group_by = r.join(); + } + + if (sources_scope.source && (sources_scope.source.value === 'ec2' || sources_scope.source.value === 'custom')) { // for ec2, validate variable data data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.source_vars, true); } From f36885b2559dcdaf61f93de431f25eff413473a0 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 26 Nov 2014 09:37:30 -0500 Subject: [PATCH 08/15] forms/Sources.js fixed a couple small errors from church's commit for ec2 tags --- awx/ui/static/js/forms/Source.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/static/js/forms/Source.js b/awx/ui/static/js/forms/Source.js index 9fc9db75fd..ab5635ee54 100644 --- a/awx/ui/static/js/forms/Source.js +++ b/awx/ui/static/js/forms/Source.js @@ -79,7 +79,7 @@ angular.module('SourceFormDefinition', []) "
key-name=staging, key-name=production
\n" + "Limit to hosts where the Name tag begins with test:
\n" + "
tag:Name=test*
\n" + - "

View the DescribeInstances documentation " + + "

View the Describe Instances documentation " + "for a complete list of supported filters.

", dataContainer: 'body' }, @@ -106,7 +106,7 @@ angular.module('SourceFormDefinition', []) "

If blank, all groups above are created except Instance ID.

", dataContainer: 'body' }, - custom_script: { + source_script: { label : "Custom Inventory Scripts", type: 'lookup', ngShow: "source && source.value !== '' && source.value === 'custom'", From 6f1f06541421032db1862d7ea76d4c72bf0ea0a9 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 26 Nov 2014 09:45:02 -0500 Subject: [PATCH 09/15] Custom Inventory Scripts Changed the title of the modal to be a plural noun instead of singular ('Script') --- awx/ui/static/js/helpers/CustomInventory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/static/js/helpers/CustomInventory.js b/awx/ui/static/js/helpers/CustomInventory.js index 93cd236e2b..fa869559e1 100644 --- a/awx/ui/static/js/helpers/CustomInventory.js +++ b/awx/ui/static/js/helpers/CustomInventory.js @@ -84,7 +84,7 @@ angular.module('CreateCustomInventoryHelper', [ 'Utilities', 'RestServices', 'Sc CreateDialog({ id: 'custom-script-dialog', - title: 'Inventory Script', + title: 'Inventory Scripts', target: 'custom-script-dialog', scope: scope, buttons: buttons, From fb8c33b97334b2c117055320d454d71b2d292e93 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 26 Nov 2014 10:16:36 -0500 Subject: [PATCH 10/15] Update job template launch docs with credential_needed_to_start documentation --- awx/api/templates/api/job_template_launch.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/api/templates/api/job_template_launch.md b/awx/api/templates/api/job_template_launch.md index bbee46ebcc..baf5cd6990 100644 --- a/awx/api/templates/api/job_template_launch.md +++ b/awx/api/templates/api/job_template_launch.md @@ -14,6 +14,9 @@ The response will include the following fields: job_template (array, read-only) * `survey_enabled`: Flag indicating if whether the job_template has an enabled survey (boolean, read-only) +* `credential_needed_to_start`: Flag indicating the presence of a credential + associated with the job template. If not then one should be supplied when + launching the job (boolean, read-only) Make a POST request to this resource to launch the job_template. If any passwords or variables are required, they must be passed via POST data. From 15e6aa29a77f87f2d0b415a4f94b54ef51fbf92f Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 26 Nov 2014 11:14:26 -0500 Subject: [PATCH 11/15] Make sure we propogate source_script into the related fields it needs to be in --- awx/api/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 3aea7ddc6e..4a5506669b 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -967,6 +967,8 @@ class InventorySourceOptionsSerializer(BaseSerializer): if obj.credential and obj.credential.active: res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,)) + if obj.source_script and obj.source_script.active: + res['source_script'] = reverse('api:inventory_script_detail', args=(obj.source_script.pk,)) return res def validate_source(self, attrs, source): @@ -1042,8 +1044,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) if obj.group and obj.group.active: res['group'] = reverse('api:group_detail', args=(obj.group.pk,)) - if obj.source_script: - res['source_script'] = reverse('api:inventory_script_detail', args=(obj.source_script.pk,)) # Backwards compatibility. if obj.current_update: res['current_update'] = reverse('api:inventory_update_detail', From f8c6aa6fca36b5848f12a301b470605c3d9138b0 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 26 Nov 2014 11:37:36 -0500 Subject: [PATCH 12/15] Roll this back, it doesn't do what I expect --- awx/main/models/unified_jobs.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 157b46a3bb..dd5ea06933 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -295,10 +295,6 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): kwargs.pop('%s_id' % parent_field_name, None) kwargs[parent_field_name] = self for field_name in self._get_unified_job_field_names(): - if hasattr(self, '%s_id' % field_name) and field_name in kwargs: - kwargs['%s_id' % field_name] = kwargs[field_name] - kwargs.pop(field_name) - continue if field_name in kwargs: continue # Foreign keys can be specified as field_name or field_name_id. From e1f25a09499b0cdea8aa9735238ef8ebb8e459ab Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 26 Nov 2014 11:54:49 -0500 Subject: [PATCH 13/15] Fix up unified job creation parameter passing --- awx/main/models/unified_jobs.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index dd5ea06933..4c6bc3d235 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -293,16 +293,19 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): unified_job_class = self._get_unified_job_class() parent_field_name = unified_job_class._get_parent_field_name() kwargs.pop('%s_id' % parent_field_name, None) - kwargs[parent_field_name] = self + create_kwargs = {} + create_kwargs[parent_field_name] = self for field_name in self._get_unified_job_field_names(): if field_name in kwargs: + create_kwargs[field_name] = kwargs[field_name] continue # Foreign keys can be specified as field_name or field_name_id. if hasattr(self, '%s_id' % field_name) and ('%s_id' % field_name) in kwargs: + create_kwargs['%s_id' % field_name] = kwargs['%s_id' % field_name] = kwargs[field_name] continue - kwargs[field_name] = getattr(self, field_name) - kwargs = self._update_unified_job_kwargs(**kwargs) - unified_job = unified_job_class(**kwargs) + create_kwargs[field_name] = getattr(self, field_name) + kwargs = self._update_unified_job_kwargs(**create_kwargs) + unified_job = unified_job_class(**create_kwargs) if save_unified_job: unified_job.save() return unified_job From 86c717bca38d1f1ed52829f26108bd06191b5fee Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 26 Nov 2014 13:18:19 -0500 Subject: [PATCH 14/15] Job submission changed the 'credential' to 'credential_id' on the POST to the job launch endpoint, per request from Matt --- awx/ui/static/js/helpers/CustomInventory.js | 2 +- awx/ui/static/js/helpers/JobSubmission.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/static/js/helpers/CustomInventory.js b/awx/ui/static/js/helpers/CustomInventory.js index fa869559e1..93cd236e2b 100644 --- a/awx/ui/static/js/helpers/CustomInventory.js +++ b/awx/ui/static/js/helpers/CustomInventory.js @@ -84,7 +84,7 @@ angular.module('CreateCustomInventoryHelper', [ 'Utilities', 'RestServices', 'Sc CreateDialog({ id: 'custom-script-dialog', - title: 'Inventory Scripts', + title: 'Inventory Script', target: 'custom-script-dialog', scope: scope, buttons: buttons, diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index 98045da00f..f402e64ca2 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -45,7 +45,7 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential } delete(job_launch_data.extra_vars); if(!Empty(scope.credential)){ - job_launch_data.credential = scope.credential; + job_launch_data.credential_id = scope.credential; } Rest.setUrl(url); Rest.post(job_launch_data) From 1e56d3d2fd573eb8f0c0e15f66e5fb31e858b760 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 26 Nov 2014 13:34:10 -0500 Subject: [PATCH 15/15] Add tests for inventory_filters and group_by, fix to convert None to empty string. --- awx/main/models/inventory.py | 8 +++++--- awx/main/tests/inventory.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 38c8247b07..70b4dcaa9f 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -927,11 +927,12 @@ class InventorySourceOptions(BaseModel): source_vars_dict = VarsDictProperty('source_vars') def clean_instance_filters(self): + instance_filters = unicode(self.instance_filters or '') if self.source != 'ec2': return '' invalid_filters = [] instance_filter_re = re.compile(r'^(?:tag:.+)|(?:[a-z][a-z\.-]*[a-z])=.*$') - for instance_filter in self.instance_filters.split(','): + for instance_filter in instance_filters.split(','): instance_filter = instance_filter.strip() if not instance_filter: continue @@ -941,16 +942,17 @@ class InventorySourceOptions(BaseModel): raise ValidationError('Invalid filter expression%s: %s' % ('' if len(invalid_filters) == 1 else 's', ', '.join(invalid_filters))) - return self.instance_filters + return instance_filters def clean_group_by(self): + group_by = unicode(self.group_by or '') if self.source != 'ec2': return '' get_choices = getattr(self, 'get_%s_group_by_choices' % self.source) valid_choices = [x[0] for x in get_choices()] choice_transform = lambda x: x.strip().lower() valid_choices = [choice_transform(x) for x in valid_choices] - choices = [choice_transform(x) for x in self.group_by.split(',') if x.strip()] + choices = [choice_transform(x) for x in group_by.split(',') if x.strip()] invalid_choices = [] for c in choices: if c not in valid_choices and c not in invalid_choices: diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index 1d9cb517e1..036246e0bd 100644 --- a/awx/main/tests/inventory.py +++ b/awx/main/tests/inventory.py @@ -1256,10 +1256,36 @@ class InventoryUpdatesTest(BaseTransactionTest): 'source': 'ec2', 'credential': aws_cred_id, 'source_regions': '', + 'instance_filters': '', + 'group_by': '', } with self.current_user(self.super_django_user): response = self.put(inv_src_url1, inv_src_data, expect=200) self.assertEqual(response['source_regions'], '') + # Null for instance filters and group_by should be converted to empty + # string. + inv_src_data['instance_filters'] = None + inv_src_data['group_by'] = None + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=200) + self.assertEqual(response['instance_filters'], '') + self.assertEqual(response['group_by'], '') + # Invalid string for instance filters. + inv_src_data['instance_filters'] = 'tag-key_123=Name,' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=400) + # Valid string for instance filters. + inv_src_data['instance_filters'] = 'tag-key=Name' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=200) + # Invalid string for group_by. + inv_src_data['group_by'] = 'ec2_region,' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=400) + # Valid string for group_by. + inv_src_data['group_by'] = 'region,key_pair,instance_type' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=200) # All region. inv_src_data['source_regions'] = 'ALL' with self.current_user(self.super_django_user): @@ -1293,6 +1319,8 @@ class InventoryUpdatesTest(BaseTransactionTest): 'source': 'rax', 'credential': rax_cred_id, 'source_regions': '', + 'instance_filters': None, + 'group_by': None, } with self.current_user(self.super_django_user): response = self.put(inv_src_url2, inv_src_data, expect=200)