diff --git a/awx/main/access.py b/awx/main/access.py index 06aed49543..58b505dc9b 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -569,7 +569,7 @@ class ProjectAccess(BaseAccess): def get_queryset(self): qs = Project.objects.filter(active=True).distinct() - qs = qs.select_related('created_by') + qs = qs.select_related('created_by', 'current_update', 'last_update') if self.user.is_superuser: return qs allowed = [PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK] diff --git a/awx/main/management/commands/cleanup_jobs.py b/awx/main/management/commands/cleanup_jobs.py index 07708fe670..1378fafebe 100644 --- a/awx/main/management/commands/cleanup_jobs.py +++ b/awx/main/management/commands/cleanup_jobs.py @@ -14,21 +14,27 @@ from django.utils.dateparse import parse_datetime from django.utils.timezone import now, is_aware, make_aware # AWX -from awx.main.models import Job +from awx.main.models import ProjectUpdate, Job class Command(NoArgsCommand): ''' - Management command to cleanup old jobs. + Management command to cleanup old jobs and project updates. ''' - help = 'Remove old jobs and events from the database.' + help = 'Remove old jobs and project updates from the database.' option_list = NoArgsCommand.option_list + ( make_option('--days', dest='days', type='int', default=90, metavar='N', - help='Remove jobs executed more than N days ago'), + help='Remove jobs/updates executed more than N days ago'), make_option('--dry-run', dest='dry_run', action='store_true', default=False, help='Dry run mode (show items that would ' 'be removed)'), + make_option('--jobs', dest='only_jobs', action='store_true', + default=False, + help='Only remove jobs (leave project updates alone)'), + make_option('--project-updates', dest='only_project_updates', + action='store_true', default=False, + help='Only remove project updates (leave jobs alone)'), ) def cleanup_jobs(self): @@ -38,7 +44,7 @@ class Command(NoArgsCommand): job_display = '"%s" (started %s, %d host summaries, %d events)' % \ (unicode(job), unicode(job.created), job.job_host_summaries.count(), job.job_events.count()) - if job.status in ('pending', 'running'): + if job.status in ('pending', 'waiting', 'running'): action_text = 'would skip' if self.dry_run else 'skipping' self.logger.debug('%s %s job %s', action_text, job.status, job_display) elif job.created >= self.cutoff: @@ -50,6 +56,24 @@ class Command(NoArgsCommand): if not self.dry_run: job.delete() + def cleanup_project_updates(self): + for pu in ProjectUpdate.objects.all(): + pu_display = '"%s" (started %s)' % (unicode(pu), unicode(pu.created)) + if pu.status in ('pending', 'waiting', 'running'): + action_text = 'would skip' if self.dry_run else 'skipping' + self.logger.debug('%s %s project update %s', action_text, pu.status, pu_display) + if pu in (pu.project.current_update, pu.project.last_update) and pu.project.scm_type: + action_text = 'would skip' if self.dry_run else 'skipping' + self.logger.debug('%s %s', action_text, pu_display) + elif pu.created >= self.cutoff: + action_text = 'would skip' if self.dry_run else 'skipping' + self.logger.debug('%s %s', action_text, pu_display) + else: + action_text = 'would delete' if self.dry_run else 'deleting' + self.logger.info('%s %s', action_text, pu_display) + if not self.dry_run: + pu.delete() + def init_logging(self): log_levels = dict(enumerate([logging.ERROR, logging.INFO, logging.DEBUG, 0])) @@ -67,4 +91,9 @@ class Command(NoArgsCommand): self.days = int(options.get('days', 90)) self.dry_run = bool(options.get('dry_run', False)) self.cutoff = now() - datetime.timedelta(days=self.days) - self.cleanup_jobs() + self.only_jobs = bool(options.get('only_jobs', False)) + self.only_project_updates = bool(options.get('only_project_updates', False)) + if self.only_jobs or (not self.only_jobs and not self.only_project_updates): + self.cleanup_jobs() + if self.only_project_updates or (not self.only_jobs and not self.only_project_updates): + self.cleanup_project_updates() diff --git a/awx/main/migrations/0010_v13_changes.py b/awx/main/migrations/0010_v13_changes.py new file mode 100644 index 0000000000..f57569f820 --- /dev/null +++ b/awx/main/migrations/0010_v13_changes.py @@ -0,0 +1,521 @@ +# -*- coding: utf-8 -*- +import 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 'ProjectUpdate.modified' + db.add_column(u'main_projectupdate', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'ProjectUpdate.modified_by' + db.add_column(u'main_projectupdate', 'modified_by', + self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name="{'class': 'projectupdate', 'app_label': 'main'}(class)s_modified+", null=True, on_delete=models.SET_NULL, to=orm['auth.User']), + keep_default=False) + + # Adding field 'Job.modified' + db.add_column(u'main_job', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'Job.modified_by' + db.add_column(u'main_job', 'modified_by', + self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name="{'class': 'job', 'app_label': 'main'}(class)s_modified+", null=True, on_delete=models.SET_NULL, to=orm['auth.User']), + keep_default=False) + + # Adding field 'Inventory.modified' + db.add_column(u'main_inventory', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'Inventory.modified_by' + db.add_column(u'main_inventory', 'modified_by', + self.gf('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=orm['auth.User']), + keep_default=False) + + # Adding field 'Host.modified' + db.add_column(u'main_host', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'Host.modified_by' + db.add_column(u'main_host', 'modified_by', + self.gf('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=orm['auth.User']), + keep_default=False) + + # Adding field 'JobHostSummary.created' + db.add_column(u'main_jobhostsummary', 'created', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now_add=True, blank=True), + keep_default=False) + + # Adding field 'JobHostSummary.modified' + db.add_column(u'main_jobhostsummary', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'Group.modified' + db.add_column(u'main_group', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'Group.modified_by' + db.add_column(u'main_group', 'modified_by', + self.gf('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=orm['auth.User']), + keep_default=False) + + # Adding field 'Credential.modified' + db.add_column(u'main_credential', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'Credential.modified_by' + db.add_column(u'main_credential', 'modified_by', + self.gf('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=orm['auth.User']), + keep_default=False) + + # Adding field 'JobTemplate.modified' + db.add_column(u'main_jobtemplate', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'JobTemplate.modified_by' + db.add_column(u'main_jobtemplate', 'modified_by', + self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name="{'class': 'jobtemplate', 'app_label': 'main'}(class)s_modified+", null=True, on_delete=models.SET_NULL, to=orm['auth.User']), + keep_default=False) + + # Adding field 'Team.modified' + db.add_column(u'main_team', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'Team.modified_by' + db.add_column(u'main_team', 'modified_by', + self.gf('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=orm['auth.User']), + keep_default=False) + + # Adding field 'JobEvent.modified' + db.add_column(u'main_jobevent', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'Project.modified' + db.add_column(u'main_project', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'Project.modified_by' + db.add_column(u'main_project', 'modified_by', + self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name="{'class': 'project', 'app_label': u'main'}(class)s_modified+", null=True, on_delete=models.SET_NULL, to=orm['auth.User']), + keep_default=False) + + # Adding field 'Project.current_update' + db.add_column(u'main_project', 'current_update', + self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='project_as_current_update+', null=True, to=orm['main.ProjectUpdate']), + keep_default=False) + + + # Changing field 'Project.scm_url' + db.alter_column(u'main_project', 'scm_url', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True)) + # Adding field 'Organization.modified' + db.add_column(u'main_organization', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'Organization.modified_by' + db.add_column(u'main_organization', 'modified_by', + self.gf('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=orm['auth.User']), + keep_default=False) + + # Adding field 'Permission.modified' + db.add_column(u'main_permission', 'modified', + self.gf('django.db.models.fields.DateTimeField')(default=now, auto_now=True, blank=True), + keep_default=False) + + # Adding field 'Permission.modified_by' + db.add_column(u'main_permission', 'modified_by', + self.gf('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=orm['auth.User']), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'ProjectUpdate.modified' + db.delete_column(u'main_projectupdate', 'modified') + + # Deleting field 'ProjectUpdate.modified_by' + db.delete_column(u'main_projectupdate', 'modified_by_id') + + # Deleting field 'Job.modified' + db.delete_column(u'main_job', 'modified') + + # Deleting field 'Job.modified_by' + db.delete_column(u'main_job', 'modified_by_id') + + # Deleting field 'Inventory.modified' + db.delete_column(u'main_inventory', 'modified') + + # Deleting field 'Inventory.modified_by' + db.delete_column(u'main_inventory', 'modified_by_id') + + # Deleting field 'Host.modified' + db.delete_column(u'main_host', 'modified') + + # Deleting field 'Host.modified_by' + db.delete_column(u'main_host', 'modified_by_id') + + # Deleting field 'JobHostSummary.created' + db.delete_column(u'main_jobhostsummary', 'created') + + # Deleting field 'JobHostSummary.modified' + db.delete_column(u'main_jobhostsummary', 'modified') + + # Deleting field 'Group.modified' + db.delete_column(u'main_group', 'modified') + + # Deleting field 'Group.modified_by' + db.delete_column(u'main_group', 'modified_by_id') + + # Deleting field 'Credential.modified' + db.delete_column(u'main_credential', 'modified') + + # Deleting field 'Credential.modified_by' + db.delete_column(u'main_credential', 'modified_by_id') + + # Deleting field 'JobTemplate.modified' + db.delete_column(u'main_jobtemplate', 'modified') + + # Deleting field 'JobTemplate.modified_by' + db.delete_column(u'main_jobtemplate', 'modified_by_id') + + # Deleting field 'Team.modified' + db.delete_column(u'main_team', 'modified') + + # Deleting field 'Team.modified_by' + db.delete_column(u'main_team', 'modified_by_id') + + # Deleting field 'JobEvent.modified' + db.delete_column(u'main_jobevent', 'modified') + + # Deleting field 'Project.modified' + db.delete_column(u'main_project', 'modified') + + # Deleting field 'Project.modified_by' + db.delete_column(u'main_project', 'modified_by_id') + + # Deleting field 'Project.current_update' + db.delete_column(u'main_project', 'current_update_id') + + + # Changing field 'Project.scm_url' + db.alter_column(u'main_project', 'scm_url', self.gf('django.db.models.fields.URLField')(max_length=1024, null=True)) + # Deleting field 'Organization.modified' + db.delete_column(u'main_organization', 'modified') + + # Deleting field 'Organization.modified_by' + db.delete_column(u'main_organization', 'modified_by_id') + + # Deleting field 'Permission.modified' + db.delete_column(u'main_permission', 'modified') + + # Deleting field 'Permission.modified_by' + db.delete_column(u'main_permission', 'modified_by_id') + + + 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.credential': { + 'Meta': {'object_name': 'Credential'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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'}), + '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'}), + 'ssh_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'ssh_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Team']", 'blank': 'True', 'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['auth.User']", 'blank': 'True', 'null': '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', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'}), + 'has_active_failures': ('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']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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']"}), + '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', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Job']", 'blank': 'True', 'null': 'True'}), + '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': u"orm['main.JobHostSummary']", 'blank': 'True', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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']"}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}) + }, + 'main.job': { + 'Meta': {'object_name': 'Job'}, + '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', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'job\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Credential']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobs'", 'blank': 'True', 'through': u"orm['main.JobHostSummary']", 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_args': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + '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'}), + 'launch_type': ('django.db.models.fields.CharField', [], {'default': "'manual'", 'max_length': '20'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'job\', \'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'}), + 'playbook': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}), + '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': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}), + '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', [], {'related_name': "'job_events_as_primary_host'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Host']", 'blank': 'True', 'null': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'job_events'", 'blank': 'True', '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': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'children'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobEvent']", 'blank': 'True', 'null': 'True'}), + 'play': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}) + }, + u'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': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}), + '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': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'jobtemplate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': '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'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", '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'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'jobtemplate\', \'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'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}), + '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', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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': u"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', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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': u"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']"}) + }, + u'main.project': { + 'Meta': {'object_name': 'Project'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'project\', \'app_label\': u\'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'current_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'project_as_current_update+'", 'null': 'True', 'to': "orm['main.ProjectUpdate']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'project_as_last_update+'", 'null': 'True', 'to': "orm['main.ProjectUpdate']"}), + 'last_update_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'project\', \'app_label\': u\'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'}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'null': 'True', '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_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'scm_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'null': 'True', 'blank': 'True'}), + 'scm_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'null': 'True', 'blank': 'True'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'null': 'True', 'blank': 'True'}), + 'scm_update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'null': 'True', 'blank': 'True'}), + 'scm_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'null': 'True', 'blank': 'True'}) + }, + 'main.projectupdate': { + 'Meta': {'object_name': 'ProjectUpdate'}, + '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', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'projectupdate\', \'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'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job_args': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'projectupdate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_updates'", 'to': u"orm['main.Project']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}) + }, + 'main.team': { + 'Meta': {'object_name': 'Team'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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', [], {'unique': 'True', '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': u"orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 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/__init__.py b/awx/main/models/__init__.py index 3b9db1f99c..4e32421ca4 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -2,6 +2,7 @@ # All Rights Reserved. # Python +import datetime import hmac import json import logging @@ -19,7 +20,7 @@ from django.db.models import CASCADE, SET_NULL, PROTECT from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from django.contrib.auth.models import User -from django.utils.timezone import now +from django.utils.timezone import now, make_aware, get_default_timezone # Django-JSONField from jsonfield import JSONField @@ -67,6 +68,7 @@ PERMISSION_TYPE_CHOICES = [ JOB_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. @@ -85,11 +87,16 @@ class PrimordialModel(models.Model): abstract = True description = models.TextField(blank=True, default='') - created_by = models.ForeignKey('auth.User', - on_delete=SET_NULL, null=True, - related_name='%s(class)s_created', - editable=False) # not blank=False on purpose for admin! created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True, default=now) + created_by = models.ForeignKey('auth.User', + related_name='%s(class)s_created+', + default=None, null=True, editable=False, + on_delete=models.SET_NULL) + modified_by = models.ForeignKey('auth.User', + related_name='%s(class)s_modified+', + default=None, null=True, editable=False, + on_delete=models.SET_NULL) active = models.BooleanField(default=True) tags = TaggableManager(blank=True) @@ -522,9 +529,6 @@ class Project(CommonModel): local_path = models.CharField( max_length=1024, - # Not unique for now, otherwise "deletes" won't allow reusing the - # same path for another active project. - #unique=True, blank=True, help_text=_('Local path (relative to PROJECTS_ROOT) containing ' 'playbooks and related files for this project.') @@ -538,7 +542,7 @@ class Project(CommonModel): default='', verbose_name=_('SCM Type'), ) - scm_url = models.URLField( + scm_url = models.CharField( max_length=1024, blank=True, null=True, @@ -598,6 +602,13 @@ class Project(CommonModel): help_text=_('Passphrase to unlock SSH private key if encrypted (or ' '"ASK" to prompt the user).'), ) + current_update = models.ForeignKey( + 'ProjectUpdate', + null=True, + default=None, + editable=False, + related_name='project_as_current_update+', + ) last_update = models.ForeignKey( 'ProjectUpdate', null=True, @@ -611,12 +622,8 @@ class Project(CommonModel): ) # FIXME: Still need to implement: - # - some scm_url validation # - scm_update_on_launch - # - prompt for passwords for project update - # - prompt for passwords when running job when scm_update_on_launch set # - prevent simultaneous updates of project and running jobs using project - # - prevent manually setting local path when scm_type is set # - masking passwords in project update args/stdout def save(self, *args, **kwargs): @@ -650,8 +657,45 @@ class Project(CommonModel): needed.append(field) return needed - def update(self, **kwargs): + @property + def status(self): + # FIXME: Update status values! if self.scm_type: + if self.current_update: + return 'updating' + elif not self.last_update: + return 'never updated' + elif self.last_update_failed: + return 'failed' + elif not self.get_project_path(): + return 'missing' + else: + return 'successsful' + elif not self.get_project_path(): + return 'missing' + else: + return 'ok' + + @property + def last_updated(self): + if self.scm_type and self.last_update: + return self.last_update.modified + else: + project_path = self.get_project_path() + if project_path: + try: + mtime = os.path.getmtime(project_path) + dt = datetime.datetime.fromtimestamp(mtime) + return make_aware(dt, get_default_timezone()) + except os.error: + pass + + @property + def can_update(self): + return bool(self.scm_type and not self.current_update) + + def update(self, **kwargs): + if self.can_update: needed = self.scm_passwords_needed opts = dict([(field, kwargs.get(field, '')) for field in needed]) if not all(opts.values()): @@ -660,10 +704,6 @@ class Project(CommonModel): project_update.start(**opts) return project_update - @property - def active_updates(self): - return self.project_updates.filter(active=True, status__in=('new', 'pending', 'running')) - def get_absolute_url(self): return reverse('main:project_detail', args=(self.pk,)) @@ -779,15 +819,24 @@ class ProjectUpdate(PrimordialModel): status_before = project_update_before.status self.failed = bool(self.status in ('failed', 'error', 'canceled')) super(ProjectUpdate, self).save(*args, **kwargs) - # If status changed, and update has completed, update project. + # If status changed, update project. if self.status != status_before: - if self.status in ('successful', 'failed', 'error', 'canceled'): + if self.status in ('pending', 'waiting', 'running'): project = self.project + if project.current_update != self: + project.current_update = self + project.save(update_fields=['current_update']) + elif self.status in ('successful', 'failed', 'error', 'canceled'): + project = self.project + if project.current_update == self: + project.current_update = None project.last_update = self project.last_update_failed = self.failed if not self.failed and project.scm_delete_on_next_update: project.scm_delete_on_next_update = False - project.save() + project.save(update_fields=['current_update', 'last_update', + 'last_update_failed', + 'scm_delete_on_next_update']) def get_absolute_url(self): return reverse('main:project_update_detail', args=(self.pk,)) @@ -800,10 +849,6 @@ class ProjectUpdate(PrimordialModel): except TaskMeta.DoesNotExist: pass - def get_passwords_needed_to_start(self): - '''Return list of password field names needed to start the job.''' - return (self.credential and self.credential.passwords_needed) or [] - @property def can_start(self): return bool(self.status == 'new') @@ -830,7 +875,7 @@ class ProjectUpdate(PrimordialModel): @property def can_cancel(self): - return bool(self.status in ('pending', 'running')) + return bool(self.status in ('pending', 'waiting', 'running')) def cancel(self): if self.can_cancel: @@ -1151,9 +1196,15 @@ class Job(CommonModelNameNotUnique): h = hmac.new(settings.SECRET_KEY, self.created.isoformat()) return '%d-%s' % (self.pk, h.hexdigest()) - def get_passwords_needed_to_start(self): + @property + def passwords_needed_to_start(self): '''Return list of password field names needed to start the job.''' - return (self.credential and self.credential.passwords_needed) or [] + needed = [] + if self.credential: + needed.extend(self.credential.passwords_needed) + if self.project.scm_update_on_launch: + needed.extend(self.project.scm_passwords_needed) + return needed @property def can_start(self): @@ -1163,7 +1214,7 @@ class Job(CommonModelNameNotUnique): from awx.main.tasks import RunJob if not self.can_start: return False - needed = self.get_passwords_needed_to_start() + needed = self.passwords_needed_to_start opts = dict([(field, kwargs.get(field, '')) for field in needed]) if not all(opts.values()): return False @@ -1181,7 +1232,7 @@ class Job(CommonModelNameNotUnique): @property def can_cancel(self): - return bool(self.status in ('pending', 'running')) + return bool(self.status in ('pending', 'waiting', 'running')) def cancel(self): if self.can_cancel: @@ -1245,6 +1296,14 @@ class JobHostSummary(models.Model): on_delete=models.CASCADE, editable=False, ) + created = models.DateTimeField( + auto_now_add=True, + default=now, + ) + modified = models.DateTimeField( + auto_now=True, + default=now, + ) changed = models.PositiveIntegerField(default=0, editable=False) dark = models.PositiveIntegerField(default=0, editable=False) @@ -1353,6 +1412,11 @@ class JobEvent(models.Model): ) created = models.DateTimeField( auto_now_add=True, + default=now, + ) + modified = models.DateTimeField( + auto_now=True, + default=now, ) event = models.CharField( max_length=100, diff --git a/awx/main/serializers.py b/awx/main/serializers.py index 82546d81d9..0a86bf7cdc 100644 --- a/awx/main/serializers.py +++ b/awx/main/serializers.py @@ -3,6 +3,7 @@ # Python import json +import urlparse # PyYAML import yaml @@ -21,8 +22,8 @@ from rest_framework import serializers # AWX from awx.main.models import * -BASE_FIELDS = ('id', 'url', 'related', 'summary_fields', 'created', 'name', - 'description') +BASE_FIELDS = ('id', 'url', 'related', 'summary_fields', 'created', 'modified', + 'name', 'description') # objects that if found we should add summary info for them SUMMARIZABLE_FKS = ( @@ -43,6 +44,7 @@ class BaseSerializer(serializers.ModelSerializer): # make certain fields read only created = serializers.SerializerMethodField('get_created') + modified = serializers.SerializerMethodField('get_modified') active = serializers.SerializerMethodField('get_active') def get_fields(self): @@ -63,6 +65,9 @@ class BaseSerializer(serializers.ModelSerializer): elif key == 'created': field.help_text = 'Timestamp when this %s was created.' % unicode(opts.verbose_name) field.type_label = 'datetime' + elif key == 'modified': + field.help_text = 'Timestamp when this %s was last modified.' % unicode(opts.verbose_name) + field.type_label = 'datetime' return ret def get_url(self, obj): @@ -101,6 +106,12 @@ class BaseSerializer(serializers.ModelSerializer): else: return obj.created + def get_modified(self, obj): + if isinstance(obj, User): + return obj.last_login # Not actually exposed for User. + else: + return obj.modified + def get_active(self, obj): if isinstance(obj, User): return obj.is_active @@ -182,6 +193,8 @@ class ProjectSerializer(BaseSerializer): playbooks = serializers.Field(source='playbooks', help_text='Array of playbooks available within this project.') scm_delete_on_next_update = serializers.Field(source='scm_delete_on_next_update') + status = serializers.Field(source='status') + last_updated = serializers.Field(source='last_updated') class Meta: model = Project @@ -190,7 +203,7 @@ class ProjectSerializer(BaseSerializer): 'scm_delete_on_update', 'scm_delete_on_next_update', 'scm_update_on_launch', 'scm_username', 'scm_password', 'scm_key_data', - 'scm_key_unlock', 'last_update_failed') + 'scm_key_unlock', 'last_update_failed', 'status', 'last_updated') def get_related(self, obj): res = super(ProjectSerializer, self).get_related(obj) @@ -201,6 +214,9 @@ class ProjectSerializer(BaseSerializer): update = reverse('main:project_update_view', args=(obj.pk,)), project_updates = reverse('main:project_updates_list', args=(obj.pk,)), )) + if obj.current_update: + res['current_update'] = reverse('main:project_update_detail', + args=(obj.current_update.pk,)) if obj.last_update: res['last_update'] = reverse('main:project_update_detail', args=(obj.last_update.pk,)) @@ -208,13 +224,32 @@ class ProjectSerializer(BaseSerializer): def validate_local_path(self, attrs, source): # Don't allow assigning a local_path used by another project. + # Don't allow assigning a local_path when scm_type is set. + print attrs, source, self.object valid_local_paths = Project.get_local_path_choices() if self.object: - valid_local_paths.append(self.object.local_path) + scm_type = attrs.get('scm_type', self.object.scm_type) + if not scm_type: + valid_local_paths.append(self.object.local_path) + else: + scm_type = attrs.get('scm_type', '') + if scm_type: + attrs.pop(source, None) if source in attrs and attrs[source] not in valid_local_paths: raise serializers.ValidationError('Invalid path choice') return attrs + def validate_scm_url(self, attrs, source): + if self.object: + scm_type = attrs.get('scm_type', self.object.scm_type) + else: + scm_type = attrs.get('scm_type', '') + scm_url = unicode(attrs.get(source, None) or '') + scm_url_parts = urlparse.urlsplit(scm_url) + if scm_type and not any(scm_url_parts): + raise serializers.ValidationError('SCM URL must be provided') + return attrs + class ProjectPlaybooksSerializer(ProjectSerializer): class Meta: @@ -230,7 +265,7 @@ class ProjectUpdateSerializer(BaseSerializer): class Meta: model = ProjectUpdate fields = ('id', 'url', 'related', 'summary_fields', 'created', - 'project', 'status', 'failed', 'result_stdout', + 'modified', 'project', 'status', 'failed', 'result_stdout', 'result_traceback', 'job_args', 'job_cwd', 'job_env') def get_related(self, obj): @@ -530,9 +565,9 @@ class JobHostSummarySerializer(BaseSerializer): class Meta: model = JobHostSummary - fields = ('id', 'url', 'job', 'host', 'summary_fields', 'related', - 'changed', 'dark', 'failures', 'ok', 'processed', 'skipped', - 'failed') + fields = ('id', 'url', 'job', 'host', 'created', 'modified', + 'summary_fields', 'related', 'changed', 'dark', 'failures', + 'ok', 'processed', 'skipped', 'failed') def get_related(self, obj): res = super(JobHostSummarySerializer, self).get_related(obj) @@ -549,9 +584,10 @@ class JobEventSerializer(BaseSerializer): class Meta: model = JobEvent - fields = ('id', 'url', 'created', 'job', 'event', 'event_display', - 'event_data', 'event_level', 'failed', 'changed', 'host', - 'related', 'summary_fields', 'parent', 'play', 'task') + fields = ('id', 'url', 'created', 'modified', 'job', 'event', + 'event_display', 'event_data', 'event_level', 'failed', + 'changed', 'host', 'related', 'summary_fields', 'parent', + 'play', 'task') def get_related(self, obj): res = super(JobEventSerializer, self).get_related(obj) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 3b56780e75..350fe03cb6 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -30,10 +30,13 @@ __all__ = ['RunJob', 'RunProjectUpdate'] logger = logging.getLogger('awx.main.tasks') +# FIXME: Cleanly cancel task when celery worker is stopped. + class BaseTask(Task): name = None model = None + idle_timeout = None def update_model(self, pk, **updates): ''' @@ -132,10 +135,10 @@ class BaseTask(Task): expect_passwords[n] = passwords.get(item[1], '') or '' expect_list.extend([pexpect.TIMEOUT, pexpect.EOF]) while child.isalive(): - result_id = child.expect(expect_list, timeout=2) + result_id = child.expect(expect_list, timeout=5) if result_id in expect_passwords: child.sendline(expect_passwords[result_id]) - updates = {} + updates = {'status': 'running'} if logfile_pos != logfile.tell(): logfile_pos = logfile.tell() updates['result_stdout'] = logfile.getvalue() @@ -144,11 +147,11 @@ class BaseTask(Task): if instance.cancel_flag: child.close(True) canceled = True - elif (time.time() - last_stdout_update) > 30: # FIXME: Configurable idle timeout? - print 'no updates...' - # print 'canceling...' - # child.close(True) - # canceled = True + # FIXME: Configurable idle timeout? Find a way to determine if task + # is hung waiting at a prompt. + if self.idle_timeout and (time.time() - last_stdout_update) > self.idle_timeout: + child.close(True) + canceled = True if canceled: status = 'canceled' elif child.exitstatus == 0: @@ -166,10 +169,17 @@ class BaseTask(Task): return False return True + def post_run_hook(self, instance): + ''' + Hook for actions after job/task has completed. + ''' + def run(self, pk, **kwargs): ''' Run the job/task using ansible-playbook and capture its output. ''' + self.pk = pk + self.kwargs = dict(kwargs.items()) instance = self.update_model(pk) if not self.pre_run_check(instance, **kwargs): return @@ -193,8 +203,9 @@ class BaseTask(Task): os.remove(kwargs['ssh_key_path']) except IOError: pass - self.update_model(pk, status=status, result_stdout=stdout, - result_traceback=tb) + instance = self.update_model(pk, status=status, result_stdout=stdout, + result_traceback=tb) + self.post_run_hook(instance) class RunJob(BaseTask): ''' @@ -313,12 +324,21 @@ class RunJob(BaseTask): if not super(RunJob, self).pre_run_check(job, **kwargs): return False # FIXME: Check if job is waiting on any projects that are being updated. + if job.project.has_active_updates: + pass return True + def post_run_hook(self, job): + ''' + Hook for actions after job has completed. + ''' + # Start any project updates that were blocked waiting for the job. + class RunProjectUpdate(BaseTask): name = 'run_project_update' model = ProjectUpdate + idle_timeout = 30 def build_passwords(self, project_update, **kwargs): ''' @@ -359,6 +379,8 @@ class RunProjectUpdate(BaseTask): optionally using ssh-agent for public/private key authentication. ''' args = ['ansible-playbook', '-i', 'localhost,'] + # Since we specify -vvv and tasks use async polling, we should get some + # output regularly... args.append('-%s' % ('v' * 3)) project = project_update.project scm_url = project.scm_url @@ -375,8 +397,6 @@ class RunProjectUpdate(BaseTask): 'scm_url': scm_url, 'scm_branch': scm_branch, 'scm_clean': project.scm_clean, - #'scm_username': project.scm_username, - #'scm_password': project.scm_password, 'scm_delete_on_update': scm_delete_on_update, } args.extend(['-e', json.dumps(extra_vars)]) @@ -408,4 +428,13 @@ class RunProjectUpdate(BaseTask): if not super(RunProjectUpdate, self).pre_run_check(project_update, **kwargs): return False # FIXME: Check if project update is blocked by any jobs that are being run. + project = project_update.project + if project.jobs.filter(status__in=('pending', 'waiting', 'running')): + pass return True + + def post_run_hook(self, project_update): + ''' + Hook for actions after project_update has completed. + ''' + # Start any jobs waiting on this update to finish. diff --git a/awx/main/tests/jobs.py b/awx/main/tests/jobs.py index 93d0d867f5..0114ae5b5e 100644 --- a/awx/main/tests/jobs.py +++ b/awx/main/tests/jobs.py @@ -778,6 +778,8 @@ 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 JOB_STATUS_CHOICES]): + if status == 'waiting': + continue job.status = status job.save() with self.current_user(self.user_sue): @@ -864,6 +866,8 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): # sue can cancel the job, but only when it is pending or running. for status in [x[0] for x in JOB_STATUS_CHOICES]: + if status == 'waiting': + continue job.status = status job.save() with self.current_user(self.user_sue): diff --git a/awx/main/views.py b/awx/main/views.py index 92c78447e6..9ecaf39edd 100644 --- a/awx/main/views.py +++ b/awx/main/views.py @@ -268,7 +268,7 @@ class ProjectUpdateView(GenericAPIView): def get(self, request, *args, **kwargs): obj = self.get_object() data = dict( - can_update=bool(obj.scm_type), + can_update=obj.can_update, ) if obj.scm_type: data['passwords_needed_to_update'] = obj.scm_passwords_needed @@ -276,13 +276,14 @@ class ProjectUpdateView(GenericAPIView): def post(self, request, *args, **kwargs): obj = self.get_object() - if bool(obj.scm_type): + if obj.can_update: project_update = obj.update(**request.DATA) if not project_update: data = dict(passwords_needed_to_update=obj.scm_passwords_needed) return Response(data, status=status.HTTP_400_BAD_REQUEST) else: - return Response(status=status.HTTP_202_ACCEPTED) + headers = {'Location': project_update.get_absolute_url()} + return Response(status=status.HTTP_202_ACCEPTED, headers=headers) else: return self.http_method_not_allowed(request, *args, **kwargs) @@ -796,7 +797,7 @@ class JobStart(GenericAPIView): can_start=obj.can_start, ) if obj.can_start: - data['passwords_needed_to_start'] = obj.get_passwords_needed_to_start() + data['passwords_needed_to_start'] = obj.asswords_needed_to_start return Response(data) def post(self, request, *args, **kwargs): @@ -804,7 +805,7 @@ class JobStart(GenericAPIView): if obj.can_start: result = obj.start(**request.DATA) if not result: - data = dict(passwords_needed_to_start=obj.get_passwords_needed_to_start()) + data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) return Response(data, status=status.HTTP_400_BAD_REQUEST) else: return Response(status=status.HTTP_202_ACCEPTED) diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index 4d6fcd587a..081851277f 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -20,17 +20,11 @@ - name: update project using git git: dest={{project_path}} repo={{scm_url}} version={{scm_branch}} force={{scm_clean}} when: scm_type == 'git' - async: 0 - poll: 5 - name: update project using hg hg: dest={{project_path}} repo={{scm_url}} revision={{scm_branch}} force={{scm_clean}} when: scm_type == 'hg' - async: 0 - poll: 5 - name: update project using svn subversion: dest={{project_path}} repo={{scm_url}} revision={{scm_branch}} force={{scm_clean}} when: scm_type == 'svn' - async: 0 - poll: 5 diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index ddc7df0217..d27947be5f 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -245,8 +245,8 @@ BROKER_URL = 'django://' CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TRACK_STARTED = True -CELERYD_TASK_TIME_LIMIT = 3600 -CELERYD_TASK_SOFT_TIME_LIMIT = 3540 +CELERYD_TASK_TIME_LIMIT = None +CELERYD_TASK_SOFT_TIME_LIMIT = None CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler' CELERYBEAT_MAX_LOOP_INTERVAL = 60