From 408e25442d329673af11dbf1eb75e3ee53d70ee2 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 15 Jul 2015 12:10:35 -0400 Subject: [PATCH 01/24] flake8 fixes --- tools/license-audit/license-audit.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tools/license-audit/license-audit.py b/tools/license-audit/license-audit.py index 1b5cd0ea1c..c33092a6d7 100755 --- a/tools/license-audit/license-audit.py +++ b/tools/license-audit/license-audit.py @@ -44,7 +44,15 @@ def read_requirements(towerpath): return ret def get_python(towerpath): - excludes = [ 'README*', '*.dist-info', 'funtests', 'easy_install.py', 'oslo', 'pkg_resources', '_markerlib' ] + excludes = [ + 'README*', + '*.dist-info', + 'funtests', + 'easy_install.py', + 'oslo', + 'pkg_resources', + '_markerlib' + ] directory = '%s/awx/lib/site-packages' % (towerpath,) dirlist = os.listdir(directory) ret = [] @@ -82,13 +90,13 @@ def get_js(towerpath): bowerfile.close() pkg = {} pkg['name'] = item - if pkginfo.has_key('license'): + if 'license' in pkginfo: pkg['license'] = normalize_license(pkginfo['license']) else: pkg['license'] = 'UNKNOWN' - if pkginfo.has_key('homepage'): + if 'homepage' in pkginfo: pkg['url'] = pkginfo['homepage'] - elif pkginfo.has_key('url'): + elif 'url' in pkginfo: pkg['url'] = pkginfo['url'] else: pkg['url'] = 'UNKNOWN' @@ -209,7 +217,7 @@ for req in requirements.values(): cs_info = cs.release_data(req['name'],req['version']) if not cs_info: print "Couldn't find '%s-%s'" %(req['name'],req['version']) - if not olddata.has_key(req['name']): + if 'name' not in olddata: print "... and it's not in the current data. This needs fixed!" sys.exit(1) continue @@ -233,7 +241,7 @@ for req in requirements.values(): # Update JS package info for pkg in js: - if olddata.has_key(pkg): + if 'pkg' in olddata: data = olddata[pkg] new = js_packages[pkg] if new['license'] != 'UNKNOWN' and new['license'] != data['license']: @@ -249,4 +257,4 @@ for pkg in js: olddata[pkg] = item continue -write_csv(outputfile, olddata) \ No newline at end of file +write_csv(outputfile, olddata) From c0ea174e93335ca2ca7287b72d8206304fcab5ee Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 3 Aug 2015 15:14:51 -0400 Subject: [PATCH 02/24] Rework stdout display for file storage only * Cleanup old code related to stdout download using temporary files * Remove copy-into-database code * Modify stdout download code to reference stdout file --- awx/api/serializers.py | 17 +- awx/api/views.py | 16 + awx/main/migrations/0070_v221_changes.py | 527 +++++++++++++++++++++++ awx/main/models/unified_jobs.py | 27 +- awx/main/tasks.py | 2 +- 5 files changed, 565 insertions(+), 24 deletions(-) create mode 100644 awx/main/migrations/0070_v221_changes.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 871eb88f4c..df507202d5 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -464,7 +464,7 @@ class UnifiedJobTemplateSerializer(BaseSerializer): class UnifiedJobSerializer(BaseSerializer): - result_stdout = serializers.CharField(source='result_stdout', label='result stdout', read_only=True) + result_stdout = serializers.SerializerMethodField('get_result_stdout') unified_job_template = serializers.Field(source='unified_job_template_id', label='unified job template') job_env = serializers.CharField(source='job_env', label='job env', read_only=True) @@ -557,18 +557,27 @@ class UnifiedJobListSerializer(UnifiedJobSerializer): class UnifiedJobStdoutSerializer(UnifiedJobSerializer): + result_stdout = serializers.SerializerMethodField('get_result_stdout') + class Meta: fields = ('result_stdout',) + def get_result_stdout(self, obj): + obj_size = obj.result_stdout_size + if obj_size > settings.STDOUT_MAX_BYTES_DISPLAY: + return "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size, settings.STDOUT_MAX_BYTES_DISPLAY) + return obj.result_stdout + def get_types(self): if type(self) is UnifiedJobStdoutSerializer: return ['project_update', 'inventory_update', 'job', 'ad_hoc_command', 'system_job'] else: return super(UnifiedJobStdoutSerializer, self).get_types() - def to_native(self, obj): - ret = super(UnifiedJobStdoutSerializer, self).to_native(obj) - return ret.get('result_stdout', '') + # TODO: Needed? + #def to_native(self, obj): + # ret = super(UnifiedJobStdoutSerializer, self).to_native(obj) + # return ret.get('result_stdout', '') class UserSerializer(BaseSerializer): diff --git a/awx/api/views.py b/awx/api/views.py index bb6ff67f2c..d47d89e789 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2795,6 +2795,14 @@ class UnifiedJobStdout(RetrieveAPIView): def retrieve(self, request, *args, **kwargs): unified_job = self.get_object() + obj_size = unified_job.result_stdout_size + if request.accepted_renderer.format != 'txt_download' and obj_size > settings.STDOUT_MAX_BYTES_DISPLAY: + response_message = "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size, settings.STDOUT_MAX_BYTES_DISPLAY) + if request.accepted_renderer.format == 'json': + return Response({'range': {'start': 0, 'end': 1, 'absolute_end': 1}, 'content': response_message}) + else: + return Response(response_message) + if request.accepted_renderer.format in ('html', 'api', 'json'): start_line = request.QUERY_PARAMS.get('start_line', 0) end_line = request.QUERY_PARAMS.get('end_line', None) @@ -2820,6 +2828,14 @@ class UnifiedJobStdout(RetrieveAPIView): return Response(data) elif request.accepted_renderer.format == 'ansi': return Response(unified_job.result_stdout_raw) + elif request.accepted_renderer.format == 'txt_download': + try: + content_fd = open(unified_job.result_stdout_file, 'r') + response = HttpResponse(FileWrapper(content_fd), content_type='text/plain') + response["Content-Disposition"] = 'attachment; filename="job_%s.txt"' % str(unified_job.id) + return response + except Exception, e: + return Response({"error": "Error generating stdout download file: %s" % str(e)}, status=status.HTTP_400_BAD_REQUEST) else: return super(UnifiedJobStdout, self).retrieve(request, *args, **kwargs) diff --git a/awx/main/migrations/0070_v221_changes.py b/awx/main/migrations/0070_v221_changes.py new file mode 100644 index 0000000000..698028648e --- /dev/null +++ b/awx/main/migrations/0070_v221_changes.py @@ -0,0 +1,527 @@ +# -*- coding: utf-8 -*- +import uuid +import os + +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models, connection +from django.conf import settings + +class Migration(DataMigration): + + def forwards(self, orm): + for j in orm.UnifiedJob.objects.filter(active=True): + cur = connection.cursor() + stdout_filename = os.path.join(settings.JOBOUTPUT_ROOT, "%d-%s.out" % (j.pk, str(uuid.uuid1()))) + fd = open(stdout_filename, 'w') + cur.copy_expert("copy (select result_stdout_text from main_unifiedjob where id = %d) to stdout" % j.id, fd) + fd.close() + j.result_stdout_file = stdout_filename + j.result_stdout_text = "" + j.save() + + def backwards(self, orm): + pass + + 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']"}), + 'ad_hoc_command': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.AdHocCommand']", 'symmetrical': 'False', 'blank': 'True'}), + 'changes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'credential': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Credential']", 'symmetrical': 'False', 'blank': 'True'}), + 'custom_inventory_script': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.CustomInventoryScript']", '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.adhoccommand': { + 'Meta': {'object_name': 'AdHocCommand', '_ormbases': ['main.UnifiedJob']}, + 'become_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'ad_hoc_commands'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Credential']"}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ad_hoc_commands'", 'symmetrical': 'False', 'through': "orm['main.AdHocCommandEvent']", 'to': "orm['main.Host']"}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ad_hoc_commands'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_type': ('django.db.models.fields.CharField', [], {'default': "'run'", 'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'module_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'module_name': ('django.db.models.fields.CharField', [], {'default': "'command'", '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.adhoccommandevent': { + 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('ad_hoc_command', 'host_name')]", 'object_name': 'AdHocCommandEvent'}, + 'ad_hoc_command': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ad_hoc_command_events'", 'to': "orm['main.AdHocCommand']"}), + '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': "'ad_hoc_command_events'", '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'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}) + }, + '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'}), + 'become_method': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'become_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'become_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': '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'}), + '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',)", 'unique_together': "[('name', 'organization')]", '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', [], {'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'custom_inventory_scripts'", 'to': "orm['main.Organization']"}), + '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'}), + 'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '250'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + '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']}, + 'become_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + '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', [], {'default': "'run'", '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', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Project']", 'blank': 'True', 'null': 'True'}), + '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'}), + 'become_enabled': ('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', [], {'default': "'run'", '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', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Project']", 'blank': 'True', 'null': 'True'}), + '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']"}), + 'run_ad_hoc_commands': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + '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', 'blank': 'True'}), + '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'] + symmetrical = True diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index a04ac2bdb0..117ba4a261 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -551,25 +551,6 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique if 'finished' not in update_fields: update_fields.append('finished') - # Take the output from the filesystem and record it in the - # database. - stdout = self.result_stdout_raw_handle() - if not isinstance(stdout, StringIO): - self.result_stdout_text = stdout.read() - if 'result_stdout_text' not in update_fields: - update_fields.append('result_stdout_text') - - # Attempt to delete the job output from the filesystem if it - # was moved to the database. - if self.result_stdout_file: - try: - os.remove(self.result_stdout_file) - self.result_stdout_file = '' - if 'result_stdout_file' not in update_fields: - update_fields.append('result_stdout_file') - except: - pass # Meh. We don't care that much. - # If we have a start and finished time, and haven't already calculated # out the time that elapsed, do so. if self.started and self.finished and not self.elapsed: @@ -657,6 +638,13 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique def result_stdout(self): return self._result_stdout_raw(escape_ascii=True) + @property + def result_stdout_size(self): + try: + return os.stat(self.result_stdout_file).st_size + except: + return 0 + def _result_stdout_raw_limited(self, start_line=0, end_line=None, redact_sensitive=True, escape_ascii=False): return_buffer = u"" if end_line is not None: @@ -848,3 +836,4 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique if settings.BROKER_URL.startswith('amqp://'): self._force_cancel() return self.cancel_flag + diff --git a/awx/main/tasks.py b/awx/main/tasks.py index da798a59f2..4ca8855a5c 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -493,7 +493,7 @@ class BaseTask(Task): safe_env = self.build_safe_env(instance, **kwargs) if not os.path.exists(settings.JOBOUTPUT_ROOT): os.makedirs(settings.JOBOUTPUT_ROOT) - stdout_filename = os.path.join(settings.JOBOUTPUT_ROOT, str(uuid.uuid1()) + ".out") + stdout_filename = os.path.join(settings.JOBOUTPUT_ROOT, "%d-%s.out" % (pk, str(uuid.uuid1()))) stdout_handle = codecs.open(stdout_filename, 'w', encoding='utf-8') if self.should_use_proot(instance, **kwargs): if not check_proot_installed(): From dc0d732ad4a3a6cc6a58441b4370cedf19d2aca1 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 4 Aug 2015 09:56:33 -0400 Subject: [PATCH 03/24] Fix merge issues on stdout branch The act of committing a PR that including part of these changes and then reverting it meant that some of the changes from the original branch didn't land in this new PR. This commit adds the missing bits. Lesson learned: Create a new branch when submitting a PR on a previously reverted PR --- awx/api/generics.py | 1 + awx/api/renderers.py | 3 +++ awx/api/serializers.py | 5 +++++ awx/api/templates/api/unified_job_stdout.md | 4 ++++ awx/api/views.py | 4 +++- awx/settings/defaults.py | 2 ++ 6 files changed, 18 insertions(+), 1 deletion(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 2628ce2400..d0f90c8766 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -202,6 +202,7 @@ class GenericAPIView(generics.GenericAPIView, APIView): 'model_verbose_name_plural': unicode(self.model._meta.verbose_name_plural), }) d.update({'serializer_fields': self.get_serializer().metadata()}) + d['settings'] = settings return d def metadata(self, request): diff --git a/awx/api/renderers.py b/awx/api/renderers.py index 5f3eee81db..fd60520db2 100644 --- a/awx/api/renderers.py +++ b/awx/api/renderers.py @@ -45,6 +45,9 @@ class PlainTextRenderer(renderers.BaseRenderer): data = unicode(data) return data.encode(self.charset) +class DownloadTextRenderer(PlainTextRenderer): + format = "txt_download" + class AnsiTextRenderer(PlainTextRenderer): media_type = 'text/plain' diff --git a/awx/api/serializers.py b/awx/api/serializers.py index df507202d5..460447c229 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -519,6 +519,11 @@ class UnifiedJobSerializer(BaseSerializer): ret['elapsed'] = float(ret['elapsed']) return ret + def get_result_stdout(self, obj): + obj_size = obj.result_stdout_size + if obj_size > settings.STDOUT_MAX_BYTES_DISPLAY: + return "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size, settings.STDOUT_MAX_BYTES_DISPLAY) + return obj.result_stdout class UnifiedJobListSerializer(UnifiedJobSerializer): diff --git a/awx/api/templates/api/unified_job_stdout.md b/awx/api/templates/api/unified_job_stdout.md index 70e1e1a993..63f7acea8e 100644 --- a/awx/api/templates/api/unified_job_stdout.md +++ b/awx/api/templates/api/unified_job_stdout.md @@ -12,6 +12,7 @@ Use the `format` query string parameter to specify the output format. * Plain Text: `?format=txt` * Plain Text with ANSI color codes: `?format=ansi` * JSON structure: `?format=json` +* Downloaded Plain Text: `?format=txt_download` (_New in Ansible Tower 2.0.0_) When using the Browsable API, HTML and JSON formats, the `start_line` and `end_line` query string parameters can be used @@ -20,4 +21,7 @@ to specify a range of line numbers to retrieve. Use `dark=1` or `dark=0` as a query string parameter to force or disable a dark background. ++Files over {{ settings.STDOUT_MAX_BYTES_DISPLAY|filesizeformat }} (configurable) will not display in the browser. Use the `txt_download` ++format to download the file directly to view it. + {% include "api/_new_in_awx.md" %} diff --git a/awx/api/views.py b/awx/api/views.py index d47d89e789..a0d383d758 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -25,6 +25,8 @@ from django.utils.safestring import mark_safe from django.utils.timezone import now from django.views.decorators.csrf import csrf_exempt from django.template.loader import render_to_string +from django.core.servers.basehttp import FileWrapper +from django.http import HttpResponse # Django REST Framework from rest_framework.exceptions import PermissionDenied, ParseError @@ -2789,7 +2791,7 @@ class UnifiedJobStdout(RetrieveAPIView): serializer_class = UnifiedJobStdoutSerializer renderer_classes = [BrowsableAPIRenderer, renderers.StaticHTMLRenderer, PlainTextRenderer, AnsiTextRenderer, - renderers.JSONRenderer] + renderers.JSONRenderer, DownloadTextRenderer] filter_backends = () new_in_148 = True diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index dcfdebe2a2..5caaaf06d3 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -115,6 +115,8 @@ ALLOWED_HOSTS = [] # reverse proxy. REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] +STDOUT_MAX_BYTES_DISPLAY = 1048576 + TEMPLATE_CONTEXT_PROCESSORS += ( # NOQA 'django.core.context_processors.request', 'awx.ui.context_processors.settings', From b1c12ca120c3bdca8b1db53dc65b5caf8205e54f Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 4 Aug 2015 14:23:47 -0400 Subject: [PATCH 04/24] Fix empty standard out behavior Previously we could just check for the absence of the stdout file to know we didn't yet/wouldn't ever have any stdout content. Now that we are creating the stdout file ahead of time and persisting it we need to also check to see if the file contains any data. This is also necessary for some unit tests. --- awx/main/models/unified_jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 117ba4a261..00deb50691 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -598,7 +598,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique if self.result_stdout_text: return StringIO(self.result_stdout_text) else: - if not os.path.exists(self.result_stdout_file): + if not os.path.exists(self.result_stdout_file) or os.stat(self.result_stdout_file).st_size < 1: return StringIO(msg['missing' if self.finished else 'pending']) # There is a potential timing issue here, because another From 57778eb4297eb9f64df1839cf2d087c644c3e095 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 5 Aug 2015 10:01:16 -0400 Subject: [PATCH 05/24] mock stdout unit test patch failure fix Adjust some mock.patch behavior to match the new stdout behavior where we need to check a file's size as well as its existence --- awx/main/tests/unified_jobs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/tests/unified_jobs.py b/awx/main/tests/unified_jobs.py index 607feeec4e..41dcf79d26 100644 --- a/awx/main/tests/unified_jobs.py +++ b/awx/main/tests/unified_jobs.py @@ -23,7 +23,8 @@ class UnifiedJobsUnitTest(SimpleTestCase): unified_job = UnifiedJob() unified_job.result_stdout_file = 'dummy' - result = unified_job.result_stdout_raw_handle() + with mock.patch('os.stat', st_size=1): + result = unified_job.result_stdout_raw_handle() self.assertEqual(result, 'my_file_handler') From b01c2d2ad1ebed4533ec39d9370ba76684a9c544 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 24 Jul 2015 14:44:01 -0400 Subject: [PATCH 06/24] fixes stoud formats unicode problem --- awx/lib/site-packages/README | 2 +- awx/lib/site-packages/ansiconv.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/lib/site-packages/README b/awx/lib/site-packages/README index d72e81e327..9e32d8c671 100644 --- a/awx/lib/site-packages/README +++ b/awx/lib/site-packages/README @@ -2,7 +2,7 @@ Local versions of third-party packages required by Tower. Package names and versions are listed below, along with notes on which files are included. amqp==1.4.5 (amqp/*) -ansiconv==1.0.0 (ansiconv.py) +ansiconv==1.0.0 (ansiconv.py, small fix, generate unicode html) anyjson==0.3.3 (anyjson/*) apache-libcloud==0.15.1 (libcloud/*) argparse==1.2.1 (argparse.py, needed for Python 2.6 support) diff --git a/awx/lib/site-packages/ansiconv.py b/awx/lib/site-packages/ansiconv.py index e76448f61a..d0a66df5cd 100644 --- a/awx/lib/site-packages/ansiconv.py +++ b/awx/lib/site-packages/ansiconv.py @@ -122,6 +122,7 @@ def _block_to_html(text): classes.append('ansi{0}'.format(code)) if classes: - text = '{1}'.format(' '.join(classes), text) + text = u'{1}'.format(' '.join(classes), text) return command, text + From 31b8491d532dab7ecd747a70530ef2b2fc164b38 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 5 Aug 2015 11:44:50 -0400 Subject: [PATCH 07/24] Set recursive on child process canceling Sometimes ansible spawns many subprocesses that can get orphaned and stuck if we only kill the direct descendent child processes --- awx/main/tasks.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 4ca8855a5c..39077078c1 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -432,7 +432,19 @@ class BaseTask(Task): instance = self.update_model(instance.pk) if instance.cancel_flag: try: - os.kill(child.pid, signal.SIGINT) + if settings.AWX_PROOT_ENABLED: + if not psutil: + os.kill(child.pid, signal.SIGKILL) + else: + main_proc = psutil.Process(pid=child.pid) + if hasattr(main_proc, "children"): + child_procs = main_proc.children(recursive=True) + else: + child_procs = main_proc.get_children(recursive=True) + for child_proc in child_procs: + os.kill(child_proc.pid, signal.SIGTERM) + else: + os.kill(child.pid, signal.SIGTERM) time.sleep(3) canceled = True except OSError: From 3b96c3ca524e40fb84f1ae6e3d676c7c300cfa9a Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 5 Aug 2015 11:20:48 -0400 Subject: [PATCH 08/24] Fix psutil usage on el6 for job cancel psutil will fail to import on el6 due to not being able to access a pseudo terminal. This issues a SIGKILL to the proot process in order to force the stop --- awx/main/tasks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 39077078c1..b8449d33f7 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -23,6 +23,10 @@ import uuid from distutils.version import LooseVersion as Version import dateutil.parser import yaml +try: + import psutil +except: + psutil = None # Pexpect import pexpect From f0010cc574e7d46e3fb923338df2360496a75ef9 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 4 Aug 2015 18:58:54 -0400 Subject: [PATCH 09/24] Add support for detecting encrypted openssh format private keys. Fixes https://trello.com/c/ZeVOXN5U --- awx/main/models/credential.py | 78 ++++++++++++++++++++++++++++------- awx/main/tests/projects.py | 16 ++++++- awx/main/tests/tasks.py | 59 ++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 15 deletions(-) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 5530c4670d..885538cd26 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -158,7 +158,12 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): ssh_key_data = decrypt_field(self, 'ssh_key_data') else: ssh_key_data = self.ssh_key_data - return 'ENCRYPTED' in ssh_key_data + try: + key_data = self._validate_ssh_private_key(ssh_key_data) + except ValidationError: + return False + else: + return bool(key_data['key_enc']) @property def needs_ssh_key_unlock(self): @@ -231,27 +236,52 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): """Validate that the given SSH private key or certificate is, in fact, valid. """ - cert = '' + # Map the X in BEGIN X PRIVATE KEY to the key type (ssh-keygen -t). + # Tower jobs using OPENSSH format private keys may still fail if the + # system SSH implementation lacks support for this format. + key_types = { + 'RSA': 'rsa', + 'DSA': 'dsa', + 'EC': 'ecdsa', + 'OPENSSH': 'ed25519', + '': 'rsa1', + } + # Key properties to return if valid. + key_data = { + 'key_type': None, # Key type (from above mapping). + 'key_seg': '', # Key segment (all text including begin/end). + 'key_b64': '', # Key data as base64. + 'key_bin': '', # Key data as binary. + 'key_enc': None, # Boolean, whether key is encrypted. + 'cert_seg': '', # Cert segment (all text including begin/end). + 'cert_b64': '', # Cert data as base64. + 'cert_bin': '', # Cert data as binary. + } data = data.strip() validation_error = ValidationError('Invalid private key') - # Set up the valid private key header and footer. - begin_re = r'(-{4,})\s*BEGIN\s+([A-Z0-9]+)?\s*PRIVATE\sKEY\s*(-{4,})' - end_re = r'(-{4,})\s*END\s+([A-Z0-9]+)?\s*PRIVATE\sKEY\s*(-{4,})' - # Sanity check: We may potentially receive a full PEM certificate, # and we want to accept these. cert_begin_re = r'(-{4,})\s*BEGIN\s+CERTIFICATE\s*(-{4,})' cert_end_re = r'(-{4,})\s*END\s+CERTIFICATE\s*(-{4,})' cert_begin_match = re.search(cert_begin_re, data) - if cert_begin_match: - cert_end_match = re.search(cert_end_re, data) - if not cert_end_match: + cert_end_match = re.search(cert_end_re, data) + if cert_begin_match and not cert_end_match: + raise validation_error + elif not cert_begin_match and cert_end_match: + raise validation_error + elif cert_begin_match and cert_end_match: + cert_dashes = set([cert_begin_match.groups()[0], cert_begin_match.groups()[1], + cert_end_match.groups()[0], cert_end_match.groups()[1]]) + if len(cert_dashes) != 1: raise validation_error - cert = data[cert_begin_match.start():cert_end_match.end()] + key_data['cert_seg'] = data[cert_begin_match.start():cert_end_match.end()] # Find the private key, and also ensure that it internally matches # itself. + # Set up the valid private key header and footer. + begin_re = r'(-{4,})\s*BEGIN\s+([A-Z0-9]+)?\s*PRIVATE\sKEY\s*(-{4,})' + end_re = r'(-{4,})\s*END\s+([A-Z0-9]+)?\s*PRIVATE\sKEY\s*(-{4,})' begin_match = re.search(begin_re, data) end_match = re.search(end_re, data) if not begin_match or not end_match: @@ -265,18 +295,22 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): raise validation_error if begin_match.groups()[1] != end_match.groups()[1]: raise validation_error - line_continues = False + key_type = begin_match.groups()[1] + try: + key_data['key_type'] = key_types[key_type] + except KeyError: + raise ValidationError('Invalid private key: unsupported type %s' % key_type) # The private key data begins and ends with the private key. - data = data[begin_match.start():end_match.end()] + key_data['key_seg'] = data[begin_match.start():end_match.end()] # Establish that we are able to base64 decode the private key; # if we can't, then it's not a valid key. # # If we got a certificate, validate that also, in the same way. header_re = re.compile(r'^(.+?):\s*?(.+?)(\\??)$') - base64_data = '' - for segment_to_validate in (cert, data): + for segment_name in ('cert', 'key'): + segment_to_validate = key_data['%s_seg' % segment_name] # If we have nothing; skip this one. # We've already validated that we have a private key above, # so we don't need to do it again. @@ -284,6 +318,8 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): continue # Ensure that this segment is valid base64 data. + base64_data = '' + line_continues = False lines = segment_to_validate.splitlines() for line in lines[1:-1]: line = line.strip() @@ -301,9 +337,23 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): decoded_data = base64.b64decode(base64_data) if not decoded_data: raise validation_error + key_data['%s_b64' % segment_name] = base64_data + key_data['%s_bin' % segment_name] = decoded_data except TypeError: raise validation_error + # Determine if key is encrypted. + if key_data['key_type'] == 'ed25519': + # See https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L3218 + # Decoded key data starts with magic string (null-terminated), four byte + # length field, followed by the ciphername -- if ciphername is anything + # other than 'none' the key is encrypted. + key_data['key_enc'] = not bool(key_data['key_bin'].startswith('openssh-key-v1\x00\x00\x00\x00\x04none')) + else: + key_data['key_enc'] = bool('ENCRYPTED' in key_data['key_seg']) + + return key_data + def clean_ssh_key_data(self): if self.pk: ssh_key_data = decrypt_field(self, 'ssh_key_data') diff --git a/awx/main/tests/projects.py b/awx/main/tests/projects.py index 3d4ce08b5d..6628dd3714 100644 --- a/awx/main/tests/projects.py +++ b/awx/main/tests/projects.py @@ -21,7 +21,7 @@ from django.utils.timezone import now # AWX from awx.main.models import * # noqa from awx.main.tests.base import BaseTransactionTest -from awx.main.tests.tasks import TEST_SSH_KEY_DATA, TEST_SSH_KEY_DATA_LOCKED, TEST_SSH_KEY_DATA_UNLOCK +from awx.main.tests.tasks import TEST_SSH_KEY_DATA, TEST_SSH_KEY_DATA_LOCKED, TEST_SSH_KEY_DATA_UNLOCK, TEST_OPENSSH_KEY_DATA, TEST_OPENSSH_KEY_DATA_LOCKED from awx.main.utils import decrypt_field, update_scm_url TEST_PLAYBOOK = '''- hosts: mygroup @@ -578,6 +578,20 @@ class ProjectsTest(BaseTransactionTest): data['ssh_key_data'] = TEST_SSH_KEY_DATA self.post(url, data, expect=201) + # Test with OpenSSH format private key. + with self.current_user(self.super_django_user): + data = dict(name='openssh-unlocked', user=self.super_django_user.pk, kind='ssh', + ssh_key_data=TEST_OPENSSH_KEY_DATA) + self.post(url, data, expect=201) + + # Test with OpenSSH format private key that requires passphrase. + with self.current_user(self.super_django_user): + data = dict(name='openssh-locked', user=self.super_django_user.pk, kind='ssh', + ssh_key_data=TEST_OPENSSH_KEY_DATA_LOCKED) + self.post(url, data, expect=400) + data['ssh_key_unlock'] = TEST_SSH_KEY_DATA_UNLOCK + self.post(url, data, expect=201) + # Test post as organization admin where team is part of org, but user # creating credential is not a member of the team. UI may pass user # as an empty string instead of None. diff --git a/awx/main/tests/tasks.py b/awx/main/tests/tasks.py index a14b6d02a7..20cefc1d5f 100644 --- a/awx/main/tests/tasks.py +++ b/awx/main/tests/tasks.py @@ -289,6 +289,65 @@ wwoi+P4JlJF6ZuhuDv6mhmBCSdXdc1bvimvdpOljhThr+cG5mM08iqWGKdA665cw -----END RSA PRIVATE KEY----- ''' +TEST_OPENSSH_KEY_DATA = '''-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEA1AZAwUJUiLmOXjbO5q2ZE5DF+gMpPKe8NEr12FpvOaJr1Nz/DNpf +FE/VbssOJ4CRD/6MItlPSG2pC1Cv3AYSL7NBc0YCMlBR/P/nLI8pLAzU3p3KRYvR+R6cMW +3nMcxyB1UUgzXY9dTVFIyejOsm7stGuNfdDTTLBE2vTDz6CyzxxSALEOdYut5cfeTUuG7d +nP01K3JiaHjHaXDmwraRR/JlitylaZUnSZ+/b9WCMX5FyeJ6CnGdvcCuvMK0iNjZ8R+PxP +xJBM5AlJC5J6qa8YmeaQ6lA/2S+/wGuhJmocmiXiLFy9IzIPnQiR+h8DqStp4xp245UQxe +TIGSMmq8DQAAA9A4FMRSOBTEUgAAAAdzc2gtcnNhAAABAQDUBkDBQlSIuY5eNs7mrZkTkM +X6Ayk8p7w0SvXYWm85omvU3P8M2l8UT9Vuyw4ngJEP/owi2U9IbakLUK/cBhIvs0FzRgIy +UFH8/+csjyksDNTencpFi9H5HpwxbecxzHIHVRSDNdj11NUUjJ6M6ybuy0a4190NNMsETa +9MPPoLLPHFIAsQ51i63lx95NS4bt2c/TUrcmJoeMdpcObCtpFH8mWK3KVplSdJn79v1YIx +fkXJ4noKcZ29wK68wrSI2NnxH4/E/EkEzkCUkLknqprxiZ5pDqUD/ZL7/Aa6EmahyaJeIs +XL0jMg+dCJH6HwOpK2njGnbjlRDF5MgZIyarwNAAAAAwEAAQAAAQAp8orBMYRUAJIgJavN +i67rZgslKZbw/yaHGgWFpm628mFvHcIAIvwIorrRTq8gNZl9lpjXFDNRWxDEwlPorfLPKS +Hb0pAAsE9oRKDR+gjlRCyhVop8M+t45At25A2HlrFArh5+zxp7mH4HsMJ1ktiDCgiV7W84 +e6dm1I/H/5BgwUlTNoVOGPrU183gqRsHIICjfmnjl2ObJoly+MTrAy7E9rSmsO+pHKl8z0 +XODWh3mo+EkCoYrK6kP96Jy3BepSmbZMROEsctS7Mkzu6QdnfTY3QqIzENYtTGJuAGktGj +su4MHP8hbj+TznNkFeZdmIC0uTnIKu1uquwuFF1HPZiBAAAAgACX9xPKS2J04WXpQag+JS +06n2zSuBHW7Kq4q/LMydoTRd8Quf6u6eivSBrl7H779LCtGCIZqJAslvWOyPyz2CohcCBU +emubiHcUA+aN7R9E0tyitwWraJjMIwpQ7+AbgdsLsuxozNeccSrr0tva2c5y9x7YGBcIdC +UJDt4xnBi7AAAAgQDz771v8Mb18kq5W+inDcYPFUNXGtNfeYZEOhYFpxunFnYwTEAG0Xnh +YpQXOAFZ2q5mkFQHMl4cOKwoAlaP0dM4v0JKPjFDLvGisEu95fnivj4YAMP/UHgKKxBbqW +HPUhg3adAmIJ9z9u/VmTErbVklcKWlyZuTUkxeQ/BJmSIRUQAAAIEA3oKAzdDURjy8zxLX +gBLCPdi8AxCiqQJBCsGxXCgKtZewset1XJHIN9ryfb4QSZFkSOlm/LgdeGtS8Or0GNPRYd +hgnUCF0LkEsDQ7HzPZYujLrAwjumvGQH6ORp5vRh0tQb93o4e1/A2vpdSKeH7gCe/jfUSY +h7dFGNoAI4cF7/0AAAAUcm9vdEBwaWxsb3cuaXhtbS5uZXQBAgMEBQYH +-----END OPENSSH PRIVATE KEY----- +''' + +TEST_OPENSSH_KEY_DATA_LOCKED = '''-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABALaWMfjc +hSvC7aXxQs1ZDiAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDEDWKwZD8+ +h+2gZZKna8dy2QL4jJxM1eLGDcQDnuip1ixhaf5MT5T6BMploXXHs1pfuwx8yTQ6Ts/VJp +WX6cuHQg8sPGM3P7HNGUqs9q/EQfrrRxz555uL08CRaS6FjM/6x9iolNhHU910Wlg+R+ZS +xiMrrY/s03EiEChsAWTbwBGqTopGC2xMFgIxINoQtTFXv7MtCbDfl8aWKQRDmzkLvwT07N +ycj2kqADqoukD/2bQvPrW6FIZPJPpAdeAe2SZbf/y92NgVz/glOdtjaJp3oqn1QHrOA9/k +XgXOjgVQUbzX7qyLWenxM138VsRKUJZeROaHt1MWApLrLtKQ36SrAAAD0A+PODJjfeKm3U +JknlSYD7fFh6bVZGwG6LnLMtobs0elOfj2+sdg+hOVqyrA0rPOHES5yGKslTc/wRkRQ95m +dBleAyTDIOQ90IqDxT3lsNQwpscsFKPYKGmaUvZLLk4aNY1GeANtByXwTsjetVqn8Uo59A +zu6phX8Aagn2h0qxQwBnDjlzsXf6g5H7UPZd/t1dYr1NfVP6KWJrg0jivAI8tzO2HcM9W2 +cyOaodBw/6TsJNKvDV714Z+apvrNDEufBUsovKjAna2BDVZIhTCg5mYm0Dks8JStQrG2S1 +Yk8EM3+fpo8uMoHVz1jbYC8UX12pwIU67MhUn24KBxqulCYaTMsrLFkNWk6vKgwib+sIa4 +i1Bij1Zd0rdJWypQqTc2Oj3bBSYM47AksMXcKVpuNnFLh4+eokpQzbtIYpRqhOTh1Fky7z +xkhTgWVvf/F19M9t1bz3Rm1/t5I75Ag9qfKWs06j+VVfXnDt5v5hYAEhoJjMzSjgKaqc5g +YndeWeUwO6Vijt4XpkB8+0R7Kptsh9L0UUsNIcRoGcqrM8IUVb3D8vPWppPlj9d6LB+FCo +Cy1JlscnpBb8AQy9QMvrJTHKOyjRcenVxILPiN8PypIC008jvqpDzKimAxM4IMuA7AWE6w +j5+CzfUhDAJGdl2qH/nVc7GFUtz8bVA/v9Zkawg2MLcafgGollbLcTbKwDFcenQuyHT+Hj +uDm2f0oV/EDKFqLijlV8vcLBNUZoxY/L62Vora1jlqnapq2Z/AM9NicoELYNe21ReJ5dxM +7Pk/QdSrZjQzxoHf8uBDpb7x/KyfnSdf8GmdGCxoJ5mcepwD4tROMFC104tN0STJpdGVSm +Q5ZG1JDN7F9iJCCAwyulWH/XxTzFYnQ84199cQeV/M9rXXgbXa8ApAung6X9j8y1fcw9Lw +wV1aP06bCNgM0U50PiZ54HXwzVt+Ghs06TEF4/ZQiIgNJxdw0HFxAJj8qHqUCHuSmvBgnN +qRW/uruItwpXLaL00EHu7rAFlBi1BnnetI+D12ls04mlyTUFFM5v520B5zPV+5If2hx91w +C6Oxl1Wsp3gPkK2yiuy8qcrvoEoJ25TeEhUGEAPWx2OuQJO/Lpq9aF/JJoqGwnBaXdCsi+ +5ig+ZMq5GKQtyydzyXImjlNEUH1w2prRDiGVEufANA5LSLCtqOLgDzXS62WUBjJBrQJVAM +YpWz1tiZQoyv1RT3Y0O0Vwe2Z5AK3fVM0I5jWdiLrIErtcR4ULa6T56QtA52DufhKzINTR +Vg9TtUBqfKIpRQikPSjm7vpY/Xnbc= +-----END OPENSSH PRIVATE KEY----- +''' + TEST_SSH_CERT_KEY = """-----BEGIN CERTIFICATE----- MIIDNTCCAh2gAwIBAgIBATALBgkqhkiG9w0BAQswSTEWMBQGA1UEAwwNV2luZG93 cyBBenVyZTELMAkGA1UEBhMCVVMxIjAgBgkqhkiG9w0BCQEWE2x1a2VAc25lZXJp From 1982177248310c2e50f55cd08a288e9baa69f434 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Tue, 4 Aug 2015 13:46:30 -0400 Subject: [PATCH 10/24] Fix .ini web links for v2 This moved in the github repo. --- 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 18e298f758..84cb53e90a 100644 --- a/awx/ui/static/js/forms/Source.js +++ b/awx/ui/static/js/forms/Source.js @@ -150,7 +150,7 @@ export default dataTitle: "Source Variables", dataPlacement: 'right', awPopOver: "

Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables " + - "" + + "" + "view ec2.ini in the Ansible github repo.

" + "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + "JSON:
\n" + @@ -175,7 +175,7 @@ export default dataTitle: "Source Variables", dataPlacement: 'right', awPopOver: "

Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables " + - "" + + "" + "view vmware.ini in the Ansible github repo.

" + "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + "JSON:
\n" + From 001127f8bdbe7efea8bfb7bba6595ed28360c319 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Thu, 6 Aug 2015 14:59:04 -0400 Subject: [PATCH 11/24] Limit max depth when building mapping of group depths to avoid hitting recursion limit. Fixes https://trello.com/c/2zc0odvX --- awx/main/models/inventory.py | 3 +++ .../tests/commands/commands_monolithic.py | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 10649d9c1b..3b556d04f4 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -222,6 +222,9 @@ class Inventory(CommonModel): def update_group_depths(group_pk, current_depth=0): max_depth = group_depths.get(group_pk, -1) + # Arbitrarily limit depth to avoid hitting Python recursion limit (which defaults to 1000). + if current_depth > 100: + return if current_depth > max_depth: group_depths[group_pk] = current_depth for child_pk in group_children_map.get(group_pk, set()): diff --git a/awx/main/tests/commands/commands_monolithic.py b/awx/main/tests/commands/commands_monolithic.py index 233af5dcad..3335c2825f 100644 --- a/awx/main/tests/commands/commands_monolithic.py +++ b/awx/main/tests/commands/commands_monolithic.py @@ -99,6 +99,19 @@ lb[01:09:2].example.us even_odd=odd media[0:9][0:9].example.cc ''' +TEST_INVENTORY_INI_WITH_RECURSIVE_GROUPS = '''\ +[family:children] +parent + +[parent:children] +child + +[child:children] +grandchild + +[grandchild:children] +parent +''' class BaseCommandMixin(object): ''' @@ -974,6 +987,16 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): source=self.ini_path) self.assertTrue(isinstance(result, ValueError), result) + def test_ini_file_with_recursive_groups(self): + self.create_test_ini(ini_content=TEST_INVENTORY_INI_WITH_RECURSIVE_GROUPS) + new_inv = self.organizations[0].inventories.create(name='new') + self.assertEqual(new_inv.hosts.count(), 0) + self.assertEqual(new_inv.groups.count(), 0) + result, stdout, stderr = self.run_command('inventory_import', + inventory_id=new_inv.pk, + source=self.ini_path) + self.assertEqual(result, None, stdout + stderr) + def test_executable_file(self): # Use existing inventory as source. old_inv = self.inventories[1] From 139a072afa927420ec76ac2b7756190dbbb43b50 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Thu, 6 Aug 2015 15:03:54 -0400 Subject: [PATCH 12/24] Don't create a group that is its own parent when an EC2 tag has an empty value. Fixes https://trello.com/c/2zc0odvX --- awx/plugins/inventory/ec2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/plugins/inventory/ec2.py b/awx/plugins/inventory/ec2.py index 112f5c29e8..53c699227d 100755 --- a/awx/plugins/inventory/ec2.py +++ b/awx/plugins/inventory/ec2.py @@ -527,7 +527,8 @@ class Ec2Inventory(object): 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 v: + self.push_group(self.inventory, self.to_safe("tag_" + k), key) # Inventory: Group by Route53 domain names if enabled if self.route53_enabled and self.group_by_route53_names: From 7484267186074a5c331e6e043f9ec36d5a21b744 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 7 Aug 2015 11:19:09 -0400 Subject: [PATCH 13/24] Bump 2.2 branch to version 2.2.1 --- awx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/__init__.py b/awx/__init__.py index 7454cf3beb..e3ae3d5266 100644 --- a/awx/__init__.py +++ b/awx/__init__.py @@ -5,7 +5,7 @@ import os import sys import warnings -__version__ = '2.2.0' +__version__ = '2.2.1' __all__ = ['__version__'] From 642215f9f3f07759856728469d94293770382a30 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 7 Aug 2015 11:19:59 -0400 Subject: [PATCH 14/24] add standard out download button to various places in the UI --- awx/ui/static/js/helpers/LogViewer.js | 24 ++++++++++++------- .../shared/download-standard.out.block.less | 23 ++++++++++++++++++ awx/ui/static/partials/job_stdout.html | 6 +++-- awx/ui/static/partials/job_stdout_adhoc.html | 6 +++-- 4 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 awx/ui/static/js/shared/download-standard.out.block.less diff --git a/awx/ui/static/js/helpers/LogViewer.js b/awx/ui/static/js/helpers/LogViewer.js index 41aee6384d..c04c49df93 100644 --- a/awx/ui/static/js/helpers/LogViewer.js +++ b/awx/ui/static/js/helpers/LogViewer.js @@ -3,12 +3,12 @@ * * All Rights Reserved *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:LogViewer - * @description logviewer -*/ + + /** + * @ngdoc function + * @name helpers.function:LogViewer + * @description logviewer + */ export default angular.module('LogViewerHelper', ['ModalDialog', 'Utilities', 'FormGenerator', 'VariablesHelper']) @@ -96,9 +96,12 @@ export default if (data.result_stdout) { $('#logview-tabs li:eq(1)').show(); + var showStandardOut = (data.type !== "system_job") ? true : false; AddPreFormattedText({ id: 'stdout-form-container', - val: data.result_stdout + val: data.result_stdout, + standardOut: showStandardOut, + jobUrl: data.url }); } @@ -360,8 +363,11 @@ export default return function(params) { var id = params.id, val = params.val, - html; - html = "
" + val + "
\n"; + html = ""; + if (params.standardOut) { + html += 'Download'; + } + html += "
" + val + "
\n"; $('#' + id).empty().html(html); }; }]) diff --git a/awx/ui/static/js/shared/download-standard.out.block.less b/awx/ui/static/js/shared/download-standard.out.block.less new file mode 100644 index 0000000000..984fe1b2e3 --- /dev/null +++ b/awx/ui/static/js/shared/download-standard.out.block.less @@ -0,0 +1,23 @@ +/** @define DownloadStandardOut */ + +.DownloadStandardOut { + color: #fff !important; +} + +.DownloadStandardOut--onStandardOutPage { + margin-top: -3px; + margin-right: -9px; + float: right; +} + +.DownloadStandardOut--onModal { + margin-bottom: 10px; +} + +.DownloadStandardOut-icon { + color: #fff; +} + +.DownloadStandardOut-icon--withText { + margin-right: 5px; +} diff --git a/awx/ui/static/partials/job_stdout.html b/awx/ui/static/partials/job_stdout.html index 27fe31db5e..1a4148d2cf 100644 --- a/awx/ui/static/partials/job_stdout.html +++ b/awx/ui/static/partials/job_stdout.html @@ -20,8 +20,10 @@
-
-

Standard Output

+
+

Standard Output + + Download

diff --git a/awx/ui/static/partials/job_stdout_adhoc.html b/awx/ui/static/partials/job_stdout_adhoc.html index 8d520c278b..a48b8d4df1 100644 --- a/awx/ui/static/partials/job_stdout_adhoc.html +++ b/awx/ui/static/partials/job_stdout_adhoc.html @@ -158,8 +158,10 @@
-
-

Standard Output

+
+

Standard Output + Download +

From f7b0da7be527554a536cf3d05b51710317adfc49 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 10 Aug 2015 10:02:45 -0700 Subject: [PATCH 15/24] About Tower version number fix for parsing versions such as "2.3.0" and "2.3.0-0.git201507231538" --- awx/ui/static/js/helpers/AboutAnsible.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/awx/ui/static/js/helpers/AboutAnsible.js b/awx/ui/static/js/helpers/AboutAnsible.js index 1be96d17d5..cca08fac18 100644 --- a/awx/ui/static/js/helpers/AboutAnsible.js +++ b/awx/ui/static/js/helpers/AboutAnsible.js @@ -52,17 +52,16 @@ export default scope.removeBuildAboutDialog = scope.$on('BuildAboutDialog', function(e, data) { var spaces, i, j, paddedStr = "", + versionParts, str = data.version, subscription = data.license_info.subscription_name || ""; - if(str.search('-')){ - str = str.substr(0,str.search('-')); - } - spaces = Math.floor((16-str.length)/2); + versionParts = str.split('-'); + spaces = Math.floor((16-versionParts[0].length)/2); for( i=0; i<=spaces; i++){ paddedStr = paddedStr +" "; } - paddedStr = paddedStr+str; + paddedStr = paddedStr + versionParts[0]; for( j = paddedStr.length; j<16; j++){ paddedStr = paddedStr + " "; } From 9105612c6a88587e7e13363d2d8ea6b3c84bbdc4 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 10 Aug 2015 15:42:21 -0400 Subject: [PATCH 16/24] fixed alignment of download standard out button in modals --- awx/ui/static/js/helpers/LogViewer.js | 4 +++- awx/ui/static/js/shared/download-standard.out.block.less | 5 +++++ awx/ui/static/partials/eventviewer.html | 4 ++-- awx/ui/static/partials/logviewer.html | 5 ++--- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/awx/ui/static/js/helpers/LogViewer.js b/awx/ui/static/js/helpers/LogViewer.js index c04c49df93..83261c764b 100644 --- a/awx/ui/static/js/helpers/LogViewer.js +++ b/awx/ui/static/js/helpers/LogViewer.js @@ -366,8 +366,10 @@ export default html = ""; if (params.standardOut) { html += 'Download'; + html += "
" + val + "
\n"; + } else { + html += "
" + val + "
\n"; } - html += "
" + val + "
\n"; $('#' + id).empty().html(html); }; }]) diff --git a/awx/ui/static/js/shared/download-standard.out.block.less b/awx/ui/static/js/shared/download-standard.out.block.less index 984fe1b2e3..7b8c2e9ec4 100644 --- a/awx/ui/static/js/shared/download-standard.out.block.less +++ b/awx/ui/static/js/shared/download-standard.out.block.less @@ -12,6 +12,7 @@ .DownloadStandardOut--onModal { margin-bottom: 10px; + float: right; } .DownloadStandardOut-icon { @@ -21,3 +22,7 @@ .DownloadStandardOut-icon--withText { margin-right: 5px; } + +.DownloadStandardOut-pre { + width: 100%; +} diff --git a/awx/ui/static/partials/eventviewer.html b/awx/ui/static/partials/eventviewer.html index 54302fc9d1..941e9d80d6 100644 --- a/awx/ui/static/partials/eventviewer.html +++ b/awx/ui/static/partials/eventviewer.html @@ -1,4 +1,4 @@ - -
\ No newline at end of file +
diff --git a/awx/ui/static/partials/logviewer.html b/awx/ui/static/partials/logviewer.html index 2fafebf3f7..10a105a1c2 100644 --- a/awx/ui/static/partials/logviewer.html +++ b/awx/ui/static/partials/logviewer.html @@ -1,5 +1,4 @@ - - -
\ No newline at end of file +
From 678fbd3328535a2be4ee73b716f98c0334b4be1a Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 11 Aug 2015 09:48:43 -0400 Subject: [PATCH 17/24] fixed log viewer modal partial --- awx/ui/static/partials/logviewer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/static/partials/logviewer.html b/awx/ui/static/partials/logviewer.html index 10a105a1c2..9d7dca28bb 100644 --- a/awx/ui/static/partials/logviewer.html +++ b/awx/ui/static/partials/logviewer.html @@ -1,4 +1,4 @@ -