From e1d3da731e2c65bc78ea58a3fc806ae6b96d8209 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Sun, 30 Mar 2014 22:57:45 -0400 Subject: [PATCH] AC-1040 Model cleanup/refactor, add and use job_explanation field. --- awx/api/serializers.py | 3 +- .../management/commands/run_task_system.py | 4 +- awx/main/migrations/0039_v148_changes.py | 452 ++++++++++++++++++ awx/main/models/base.py | 100 ++-- awx/main/models/credential.py | 44 +- awx/main/models/inventory.py | 13 + awx/main/models/projects.py | 2 +- awx/main/models/unified_jobs.py | 123 +++-- awx/main/tasks.py | 2 +- awx/main/tests/jobs.py | 4 +- 10 files changed, 601 insertions(+), 146 deletions(-) create mode 100644 awx/main/migrations/0039_v148_changes.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index cc84667a18..d873841b17 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -338,7 +338,8 @@ class UnifiedJobSerializer(BaseSerializer): model = UnifiedJob fields = ('*', 'unified_job_template', 'launch_type', 'status', 'failed', 'started', 'finished', 'elapsed', 'job_args', - 'job_cwd', 'job_env', 'result_stdout', 'result_traceback') + 'job_cwd', 'job_env', 'job_explanation', 'result_stdout', + 'result_traceback') def get_types(self): if type(self) is UnifiedJobSerializer: diff --git a/awx/main/management/commands/run_task_system.py b/awx/main/management/commands/run_task_system.py index 0b5f2329b8..6b0cbb6c95 100644 --- a/awx/main/management/commands/run_task_system.py +++ b/awx/main/management/commands/run_task_system.py @@ -173,7 +173,7 @@ def rebuild_graph(message): if task.celery_task_id not in active_tasks and not hasattr(settings, 'IGNORE_CELERY_INSPECTOR'): # NOTE: Pull status again and make sure it didn't finish in the meantime? task.status = 'failed' - task.result_traceback += "Task was marked as running in Tower but was not present in Celery so it has been marked as failed" + task.job_explanation += "Task was marked as running in Tower but was not present in Celery so it has been marked as failed" task.save() running_tasks.pop(running_tasks.index(task)) print("Task %s appears orphaned... marking as failed" % task) @@ -237,7 +237,7 @@ def process_graph(graph, task_capacity): start_status = node_obj.start(error_callback=error_handler) if not start_status: node_obj.status = 'failed' - node_obj.result_traceback += "Task failed pre-start check" + node_obj.job_explanation += "Task failed pre-start check" node_obj.save() # TODO: Run error handler continue diff --git a/awx/main/migrations/0039_v148_changes.py b/awx/main/migrations/0039_v148_changes.py new file mode 100644 index 0000000000..2fdaf3800a --- /dev/null +++ b/awx/main/migrations/0039_v148_changes.py @@ -0,0 +1,452 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models +from django.utils.timezone import now + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'UnifiedJob.job_explanation' + db.add_column(u'main_unifiedjob', 'job_explanation', + self.gf('django.db.models.fields.TextField')(default='', blank=True), + keep_default=False) + + # Deleting field 'Credential.ssh_key_path' + db.delete_column(u'main_credential', 'ssh_key_path') + + + # Changing field 'Schedule.dtstart' + db.alter_column(u'main_schedule', 'dtstart', self.gf('django.db.models.fields.DateTimeField')(null=True)) + + def backwards(self, orm): + # Deleting field 'UnifiedJob.job_explanation' + db.delete_column(u'main_unifiedjob', 'job_explanation') + + # Adding field 'Credential.ssh_key_path' + db.add_column(u'main_credential', 'ssh_key_path', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + + + # Changing field 'Schedule.dtstart' + db.alter_column(u'main_schedule', 'dtstart', self.gf('django.db.models.fields.DateTimeField')(default=now)) + + 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', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 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', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.activitystream': { + 'Meta': {'object_name': 'ActivityStream'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_stream'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'changes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'credential': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Credential']", 'symmetrical': 'False', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'host': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Host']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Inventory']", 'symmetrical': 'False', 'blank': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventorySource']", 'symmetrical': 'False', 'blank': 'True'}), + 'inventory_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventoryUpdate']", 'symmetrical': 'False', 'blank': 'True'}), + 'job': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Job']", 'symmetrical': 'False', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.JobTemplate']", 'symmetrical': 'False', 'blank': 'True'}), + 'object1': ('django.db.models.fields.TextField', [], {}), + 'object2': ('django.db.models.fields.TextField', [], {}), + 'object_relationship_type': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'operation': ('django.db.models.fields.CharField', [], {'max_length': '13'}), + 'organization': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Organization']", 'symmetrical': 'False', 'blank': 'True'}), + 'permission': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Project']", 'symmetrical': 'False', 'blank': 'True'}), + 'project_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.ProjectUpdate']", 'symmetrical': 'False', 'blank': 'True'}), + 'schedule': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Schedule']", 'symmetrical': 'False', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Team']", 'symmetrical': 'False', 'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'unified_job': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job+'", 'blank': 'True', 'to': "orm['main.UnifiedJob']"}), + 'unified_job_template': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job_template+'", 'blank': 'True', 'to': "orm['main.UnifiedJobTemplate']"}), + 'user': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'main.authtoken': { + 'Meta': {'object_name': 'AuthToken'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'request_hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': u"orm['auth.User']"}) + }, + 'main.credential': { + 'Meta': {'unique_together': "[('user', 'team', 'kind', 'name')]", 'object_name': 'Credential'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cloud': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 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'}), + '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'}), + 'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': u"orm['auth.User']"}), + 'username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'vault_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}) + }, + 'main.group': { + 'Meta': {'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': {'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.inventory': { + 'Meta': {'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']"}), + '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_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'}), + '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_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': {'object_name': 'Job', '_ormbases': ['main.UnifiedJob']}, + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'jobs'", 'symmetrical': 'False', 'through': "orm['main.JobHostSummary']", 'to': "orm['main.Host']"}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobTemplate']", 'blank': 'True', 'null': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 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'}), + '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']"}), + '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')]", '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', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Host']"}), + 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.jobtemplate': { + 'Meta': {'object_name': 'JobTemplate', '_ormbases': ['main.UnifiedJobTemplate']}, + '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'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'host_config_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 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': {'object_name': 'Organization'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.permission': { + 'Meta': {'object_name': 'Permission'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}) + }, + 'main.profile': { + 'Meta': {'object_name': 'Profile'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ldap_dn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.project': { + 'Meta': {'object_name': 'Project', '_ormbases': ['main.UnifiedJobTemplate']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_next_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}), + 'scm_update_cache_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'scm_update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.projectupdate': { + 'Meta': {'object_name': 'ProjectUpdate', '_ormbases': ['main.UnifiedJob']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projectupdates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_updates'", 'to': "orm['main.Project']"}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.schedule': { + 'Meta': {'ordering': "['-next_run']", 'object_name': 'Schedule'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'schedule\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'dtend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'dtstart': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 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.team': { + 'Meta': {'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'}) + }, + u'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + u'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}) + } + } + + complete_apps = ['main'] \ No newline at end of file diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 86e1c9ca41..b0047713dc 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -4,8 +4,6 @@ # Python import json import shlex -import os -import os.path # PyYAML import yaml @@ -13,9 +11,7 @@ import yaml # Django from django.conf import settings from django.db import models -from django.db import transaction from django.core.exceptions import ValidationError -from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now @@ -31,12 +27,16 @@ from taggit.managers import TaggableManager # Django-Celery from djcelery.models import TaskMeta -__all__ = ['VarsDictProperty', 'BaseModel', 'CreatedModifiedModel', 'PrimordialModel', 'CommonModel', - 'CommonModelNameNotUnique', 'CommonTask', 'PERM_INVENTORY_ADMIN', - 'PERM_INVENTORY_READ', 'PERM_INVENTORY_WRITE', - 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_CHECK', 'JOB_TYPE_CHOICES', - 'PERMISSION_TYPE_CHOICES', 'TASK_STATUS_CHOICES', - 'CLOUD_INVENTORY_SOURCES'] +# Ansible Tower +from awx.main.utils import encrypt_field + +__all__ = ['VarsDictProperty', 'BaseModel', 'CreatedModifiedModel', + 'PasswordFieldsModel', 'PrimordialModel', 'CommonModel', + 'CommonModelNameNotUnique', + 'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ', + 'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', + 'PERM_INVENTORY_CHECK', 'JOB_TYPE_CHOICES', + 'PERMISSION_TYPE_CHOICES', 'CLOUD_INVENTORY_SOURCES'] PERM_INVENTORY_ADMIN = 'admin' PERM_INVENTORY_READ = 'read' @@ -57,17 +57,6 @@ PERMISSION_TYPE_CHOICES = [ (PERM_INVENTORY_CHECK, _('Deploy To Inventory (Dry Run)')), ] -TASK_STATUS_CHOICES = [ - ('new', _('New')), # Job has been created, but not started. - ('pending', _('Pending')), # Job has been queued, but is not yet running. - ('waiting', _('Waiting')), # Job is waiting on an update/dependency. - ('running', _('Running')), # Job is currently running. - ('successful', _('Successful')), # Job completed successfully. - ('failed', _('Failed')), # Job completed, but with failures. - ('error', _('Error')), # The job was unable to run. - ('canceled', _('Canceled')), # The job was canceled before completion. -] - CLOUD_INVENTORY_SOURCES = ['ec2', 'rax'] @@ -140,7 +129,7 @@ class BaseModel(models.Model): except ValidationError, e: errors[f.name] = e.messages if errors: - raise ValidationError(errors) + raise ValidationError(errors) def update_fields(self, **kwargs): save = kwargs.pop('save', True) @@ -166,38 +155,85 @@ class BaseModel(models.Model): class CreatedModifiedModel(BaseModel): - + ''' + Common model with created/modified timestamp fields. Allows explicitly + specifying created/modified timestamps in certain cases (migrations, job + events), calculates automatically if not specified. + ''' + class Meta: abstract = True created = models.DateTimeField( - #auto_now_add=True, # FIXME: Disabled temporarily for data migration. default=None, editable=False, ) modified = models.DateTimeField( - #auto_now=True, # FIXME: Disabled temporarily for data migration. - #default=now, default=None, editable=False, ) def save(self, *args, **kwargs): update_fields = kwargs.get('update_fields', []) - # Manually perform auto_now_add and auto_now logic (for unified jobs migration). + # Manually perform auto_now_add and auto_now logic. if not self.pk and not self.created: self.created = now() if 'created' not in update_fields: update_fields.append('created') if 'modified' not in update_fields or not self.modified: - self.modified = now() # FIXME: Moved temporarily for unified jobs migration. + self.modified = now() update_fields.append('modified') super(CreatedModifiedModel, self).save(*args, **kwargs) +class PasswordFieldsModel(BaseModel): + ''' + Abstract base class for a model with password fields that should be stored + as encrypted values. + ''' + + PASSWORD_FIELDS = () + + class Meta: + abstract = True + + def _password_field_allows_ask(self, field): + return False # Override in subclasses if needed. + + def save(self, *args, **kwargs): + new_instance = not bool(self.pk) + # If update_fields has been specified, add our field names to it, + # if it hasn't been specified, then we're just doing a normal save. + update_fields = kwargs.get('update_fields', []) + # When first saving to the database, don't store any password field + # values, but instead save them until after the instance is created. + # Otherwise, store encrypted values to the database. + for field in self.PASSWORD_FIELDS: + if new_instance: + value = getattr(self, field, '') + setattr(self, '_saved_%s' % field, value) + setattr(self, field, '') + else: + ask = self._password_field_allows_ask(field) + encrypted = encrypt_field(self, field, ask) + setattr(self, field, encrypted) + if field not in update_fields: + update_fields.append(field) + super(PasswordFieldsModel, self).save(*args, **kwargs) + # After saving a new instance for the first time, set the password + # fields and save again. + if new_instance: + update_fields = [] + for field in self.PASSWORD_FIELDS: + saved_value = getattr(self, '_saved_%s' % field, '') + setattr(self, field, saved_value) + update_fields.append(field) + self.save(update_fields=update_fields) + + class PrimordialModel(CreatedModifiedModel): ''' - common model for all object types that have these standard fields + Common model for all object types that have these standard fields must use a subclass CommonModel or CommonModelNameNotUnique though as this lacks a name field. ''' @@ -275,9 +311,3 @@ class CommonModelNameNotUnique(PrimordialModel): max_length=512, unique=False, ) - - -class CommonTask(PrimordialModel): - - class Meta: - abstract = True diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 6049f93b63..d96c32eaf2 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -13,13 +13,13 @@ from django.core.exceptions import ValidationError, NON_FIELD_ERRORS from django.core.urlresolvers import reverse # AWX -from awx.main.utils import encrypt_field, decrypt_field +from awx.main.utils import decrypt_field from awx.main.models.base import * __all__ = ['Credential'] -class Credential(CommonModelNameNotUnique): +class Credential(PasswordFieldsModel, CommonModelNameNotUnique): ''' A credential contains information about how to talk to a remote resource Usually this is a SSH key location, and possibly an unlock password. @@ -86,14 +86,6 @@ class Credential(CommonModelNameNotUnique): verbose_name=_('SSH private key'), help_text=_('RSA or DSA private key to be used instead of password.'), ) - ssh_key_path = models.CharField( # FIXME: No longer needed. - editable=False, - max_length=1024, - blank=True, - default='', - verbose_name=_('SSH key path'), - help_text=_('Path to SSH private key file.'), - ) ssh_key_unlock = models.CharField( max_length=1024, blank=True, @@ -281,38 +273,16 @@ class Credential(CommonModelNameNotUnique): if errors: raise ValidationError(errors) + def _password_field_allows_ask(self, field): + return bool(self.kind == 'ssh' and field != 'ssh_key_data') + def save(self, *args, **kwargs): - new_instance = not bool(self.pk) + # If update_fields has been specified, add our field names to it, + # if hit hasn't been specified, then we're just doing a normal save. update_fields = kwargs.get('update_fields', []) - # When first saving to the database, don't store any password field - # values, but instead save them until after the instance is created. - if new_instance: - for field in self.PASSWORD_FIELDS: - value = getattr(self, field, '') - setattr(self, '_saved_%s' % field, value) - setattr(self, field, '') - # Otherwise, store encrypted values to the database. - else: - # If update_fields has been specified, add our field names to it, - # if hit hasn't been specified, then we're just doing a normal save. - for field in self.PASSWORD_FIELDS: - ask = bool(self.kind == 'ssh' and field != 'ssh_key_data') - encrypted = encrypt_field(self, field, ask) - setattr(self, field, encrypted) - if field not in update_fields: - update_fields.append(field) cloud = self.kind in ('aws', 'rax') if self.cloud != cloud: self.cloud = cloud if 'cloud' not in update_fields: update_fields.append('cloud') super(Credential, self).save(*args, **kwargs) - # After saving a new instance for the first time, set the password - # fields and save again. - if new_instance: - update_fields=[] - for field in self.PASSWORD_FIELDS: - saved_value = getattr(self, '_saved_%s' % field, '') - setattr(self, field, saved_value) - update_fields.append(field) - self.save(update_fields=update_fields) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 4f7eddaea8..beeb5ec878 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -651,6 +651,19 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions): # Do the actual save. super(InventorySource, self).save(*args, **kwargs) + def _get_current_status(self): + if self.source: + if self.current_job: + return 'running' + elif not self.last_job: + return 'never updated' + elif self.last_job_failed: + return 'failed' + else: + return 'successful' + else: + return 'none' + def get_absolute_url(self): return reverse('api:inventory_source_detail', args=(self.pk,)) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 400c31f454..aa65d5153f 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -261,7 +261,7 @@ class Project(UnifiedJobTemplate, ProjectOptions): def _get_current_status(self): if self.scm_type: if self.current_update: - return 'updating' + return 'running' elif not self.last_job: return 'never updated' elif self.last_job_failed: diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 0ad399c5ea..2783315ab1 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -15,13 +15,11 @@ import yaml from django.conf import settings from django.db import models from django.db import transaction -from django.core.exceptions import ValidationError +from django.core.exceptions import ValidationError, NON_FIELD_ERRORS from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now -from django.core.exceptions import NON_FIELD_ERRORS - # Django-JSONField from jsonfield import JSONField @@ -33,7 +31,9 @@ from djcelery.models import TaskMeta # AWX from awx.main.models.base import * -from awx.main.utils import camelcase_to_underscore, encrypt_field, decrypt_field +from awx.main.utils import decrypt_field, get_type_for_model + +__all__ = ['UnifiedJobTemplate', 'UnifiedJob'] logger = logging.getLogger('awx.main.models.unified_jobs') @@ -44,22 +44,20 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): ''' STATUS_CHOICES = [ - # from Project - ('ok', 'OK'), - ('missing', 'Missing'), - ('never updated', 'Never Updated'), - ('running', 'Running'), - ('failed', 'Failed'), - ('successful', 'Successful'), - # from InventorySource - ('none', _('No External Source')), - ('never updated', _('Never Updated')), - ('updating', _('Updating')), - #('failed', _('Failed')), - #('successful', _('Successful')), + # Common to all: + ('never updated', 'Never Updated'), # A job has never been run using this template. + ('running', 'Running'), # A job is currently running (or pending/waiting) using this template. + ('failed', 'Failed'), # The last completed job using this template failed (failed, error, canceled). + ('successful', 'Successful'), # The last completed job using this template succeeded. + # For Project only: + ('ok', 'OK'), # Project is not configured for SCM and path exists. + ('missing', 'Missing'), # Project path does not exist. + # For Inventory Source only: + ('none', _('No External Source')), # Inventory source is not configured to update from an external source. + # No longer used for Project / Inventory Source: + ('updating', _('Updating')), # Same as running. ] - class Meta: app_label = 'main' unique_together = [('polymorphic_ctype', 'name')] @@ -69,7 +67,7 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): default=None, editable=False, ) - current_job = models.ForeignKey( # alias for current_update + current_job = models.ForeignKey( 'UnifiedJob', null=True, default=None, @@ -77,7 +75,7 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): related_name='%(class)s_as_current_job+', on_delete=models.SET_NULL, ) - last_job = models.ForeignKey( # alias for last_update + last_job = models.ForeignKey( 'UnifiedJob', null=True, default=None, @@ -85,11 +83,11 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): related_name='%(class)s_as_last_job+', on_delete=models.SET_NULL, ) - last_job_failed = models.BooleanField( # alias for last_update_failed + last_job_failed = models.BooleanField( default=False, editable=False, ) - last_job_run = models.DateTimeField( # alias for last_updated + last_job_run = models.DateTimeField( null=True, default=None, editable=False, @@ -160,19 +158,19 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): exclude = [x for x in exclude if x != 'polymorphic_ctype'] return super(UnifiedJobTemplate, self).validate_unique(exclude) - @property + @property # Alias for backwards compatibility. def current_update(self): return self.current_job - @property + @property # Alias for backwards compatibility. def last_update(self): return self.last_job - @property + @property # Alias for backwards compatibility. def last_update_failed(self): return self.last_job_failed - @property + @property # Alias for backwards compatibility. def last_updated(self): return self.last_job_run @@ -198,7 +196,7 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): def _get_current_status(self): # Override in subclasses as needed. if self.current_job: - return 'updating' + return 'running' elif not self.last_job: return 'never updated' elif self.last_job_failed: @@ -267,16 +265,27 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): return unified_job -class UnifiedJob(PolymorphicModel, CommonModelNameNotUnique): +class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique): ''' Concrete base class for unified job run by the task engine. ''' + STATUS_CHOICES = [ + ('new', _('New')), # Job has been created, but not started. + ('pending', _('Pending')), # Job has been queued, but is not yet running. + ('waiting', _('Waiting')), # Job is waiting on an update/dependency. + ('running', _('Running')), # Job is currently running. + ('successful', _('Successful')), # Job completed successfully. + ('failed', _('Failed')), # Job completed, but with failures. + ('error', _('Error')), # The job was unable to run. + ('canceled', _('Canceled')), # The job was canceled before completion. + ] + LAUNCH_TYPE_CHOICES = [ - ('manual', _('Manual')), - ('callback', _('Callback')), - ('scheduled', _('Scheduled')), - ('dependency', _('Dependency')), + ('manual', _('Manual')), # Job was started manually by a user. + ('callback', _('Callback')), # Job was started via host callback. + ('scheduled', _('Scheduled')), # Job was started from a schedule. + ('dependency', _('Dependency')), # Job was started as a dependency of another job. ] PASSWORD_FIELDS = ('start_args',) @@ -322,7 +331,7 @@ class UnifiedJob(PolymorphicModel, CommonModelNameNotUnique): ) status = models.CharField( max_length=20, - choices=TASK_STATUS_CHOICES, + choices=STATUS_CHOICES, default='new', editable=False, ) @@ -361,6 +370,11 @@ class UnifiedJob(PolymorphicModel, CommonModelNameNotUnique): default={}, editable=False, ) + job_explanation = models.TextField( + blank=True, + default='', + editable=False, + ) start_args = models.TextField( blank=True, default='', @@ -396,9 +410,6 @@ class UnifiedJob(PolymorphicModel, CommonModelNameNotUnique): def _get_parent_field_name(cls): return 'unified_job_template' # Override in subclasses. - def _get_type(self): - return camelcase_to_underscore(self._meta.object_name) - def __unicode__(self): return u'%s-%s-%s' % (self.created, self.id, self.status) @@ -422,23 +433,9 @@ class UnifiedJob(PolymorphicModel, CommonModelNameNotUnique): 'last_job_failed']) def save(self, *args, **kwargs): - new_instance = not bool(self.pk) # If update_fields has been specified, add our field names to it, # if it hasn't been specified, then we're just doing a normal save. update_fields = kwargs.get('update_fields', []) - # When first saving to the database, don't store any password field - # values, but instead save them until after the instance is created. - # Otherwise, store encrypted values to the database. - for field in self.PASSWORD_FIELDS: - if new_instance: - value = getattr(self, field, '') - setattr(self, '_saved_%s' % field, value) - setattr(self, field, '') - else: - encrypted = encrypt_field(self, field) - setattr(self, field, encrypted) - if field not in update_fields: - update_fields.append(field) # Get status before save... status_before = self.status or 'new' if self.pk: @@ -472,15 +469,6 @@ class UnifiedJob(PolymorphicModel, CommonModelNameNotUnique): if 'unified_job_template' not in update_fields: update_fields.append('unified_job_template') super(UnifiedJob, self).save(*args, **kwargs) - # After saving a new instance for the first time, set the password - # fields and save again. - if new_instance: - update_fields = [] - for field in self.PASSWORD_FIELDS: - saved_value = getattr(self, '_saved_%s' % field, '') - setattr(self, field, saved_value) - update_fields.append(field) - self.save(update_fields=update_fields) # If status changed, update parent instance.... if self.status != status_before: self._update_parent_instance() @@ -538,8 +526,8 @@ class UnifiedJob(PolymorphicModel, CommonModelNameNotUnique): ''' task_class = self._get_task_class() if not self.can_start: - self.result_traceback = "Job is not in a startable status: %s, expecting one of %s" % (self.status, str(('new', 'waiting'))) - self.save() + self.job_explanation = u'%s is not in a startable status: %s, expecting one of %s' % (self._meta.verbose_name, self.status, str(('new', 'waiting'))) + self.save(update_fields=['job_explanation']) return False needed = self.get_passwords_needed_to_start() try: @@ -551,8 +539,8 @@ class UnifiedJob(PolymorphicModel, CommonModelNameNotUnique): opts = dict([(field, start_args.get(field, '')) for field in needed]) if not all(opts.values()): missing_fields = ', '.join([k for k,v in opts.items() if not v]) - self.result_traceback = "Missing needed fields: %s" % missing_fields - self.save() + self.job_explanation = u'Missing needed fields: %s' % missing_fields + self.save(update_fields=['job_explanation']) return False task_class().apply_async((self.pk,), opts, link_error=error_callback) return True @@ -571,7 +559,8 @@ class UnifiedJob(PolymorphicModel, CommonModelNameNotUnique): if not all(opts.values()): return False self.update_fields(start_args=json.dumps(kwargs), status='pending') - # notify_task_runner.delay(dict(task_type=self._get_type(), id=self.id, metadata=kwargs)) + task_type = get_type_for_model(self) + # notify_task_runner.delay(dict(task_type=task_type, id=self.id, metadata=kwargs)) return True @property @@ -605,9 +594,9 @@ class UnifiedJob(PolymorphicModel, CommonModelNameNotUnique): if instance.can_cancel: instance.status = 'canceled' update_fields = ['status'] - if not instance.result_traceback: - instance.result_traceback = 'Forced cancel' - update_fields.append('result_traceback') + if not instance.job_explanation: + instance.job_explanation = 'Forced cancel' + update_fields.append('job_explanation') instance.save(update_fields=update_fields) except: # FIXME: Log this exception! if settings.DEBUG: diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 69709d4e9b..fa19c4d02c 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -99,7 +99,7 @@ def handle_work_error(self, task_id, subtasks=None): if instance.celery_task_id != task_id: instance.status = 'failed' instance.failed = True - instance.result_traceback = "Previous Task Failed: %s for %s with celery task id: %s" % \ + instance.job_explanation = "Previous Task Failed: %s for %s with celery task id: %s" % \ (first_task_type, first_task_name, task_id) instance.save() diff --git a/awx/main/tests/jobs.py b/awx/main/tests/jobs.py index 6fa464a732..cf8ff56926 100644 --- a/awx/main/tests/jobs.py +++ b/awx/main/tests/jobs.py @@ -823,7 +823,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): # Sue can start a job (when passwords are already saved) as long as the # status is new. Reverse list so "new" will be last. - for status in reversed([x[0] for x in TASK_STATUS_CHOICES]): + for status in reversed([x[0] for x in Job.STATUS_CHOICES]): if status == 'waiting': continue job.status = status @@ -920,7 +920,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): self.check_invalid_auth(url, methods=('post',)) # sue can cancel the job, but only when it is pending or running. - for status in [x[0] for x in TASK_STATUS_CHOICES]: + for status in [x[0] for x in Job.STATUS_CHOICES]: if status == 'waiting': continue job.status = status