For AC-132. Work in progress on project SCM support.

This commit is contained in:
Chris Church
2013-08-20 16:17:30 -04:00
parent 89f5182935
commit fc68955bad
10 changed files with 1055 additions and 132 deletions

View File

@@ -173,17 +173,30 @@ class TeamAdmin(BaseModelAdmin):
list_display = ('name', 'description', 'active') list_display = ('name', 'description', 'active')
filter_horizontal = ('projects', 'users') filter_horizontal = ('projects', 'users')
class ProjectUpdateInline(admin.StackedInline):
model = ProjectUpdate
extra = 0
can_delete = True
fields = ('created', 'status', 'result_stdout')
readonly_fields = ('created', 'status', 'result_stdout')
def has_add_permission(self, request):
return False
class ProjectAdmin(BaseModelAdmin): class ProjectAdmin(BaseModelAdmin):
list_display = ('name', 'description', 'active') list_display = ('name', 'description', 'active')
fieldsets = ( fieldsets = (
(None, {'fields': (('name', 'active'), 'description', 'local_path', (None, {'fields': (('name', 'active'), 'description', 'local_path',
'get_playbooks_display')}), 'get_playbooks_display')}),
(_('SCM'), {'fields': ('scm_type', 'scm_url', 'scm_branch')}),
(_('Tags'), {'fields': ('tags',)}), (_('Tags'), {'fields': ('tags',)}),
(_('Audit'), {'fields': ('created', 'created_by',)}), (_('Audit'), {'fields': ('created', 'created_by',)}),
) )
readonly_fields = ('created', 'created_by', 'get_playbooks_display') readonly_fields = ('created', 'created_by', 'get_playbooks_display')
form = ProjectAdminForm form = ProjectAdminForm
inlines = [ProjectUpdateInline]
def get_playbooks_display(self, obj): def get_playbooks_display(self, obj):
return '<br/>'.join([format_html('{0}', x) for x in return '<br/>'.join([format_html('{0}', x) for x in

View File

@@ -0,0 +1,359 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'ProjectUpdate'
db.create_table(u'main_projectupdate', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='project_updates', to=orm['main.Project'])),
('cancel_flag', self.gf('django.db.models.fields.BooleanField')(default=False)),
('status', self.gf('django.db.models.fields.CharField')(default='new', max_length=20)),
('failed', self.gf('django.db.models.fields.BooleanField')(default=False)),
('job_args', self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True)),
('job_cwd', self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True)),
('job_env', self.gf('jsonfield.fields.JSONField')(default={}, blank=True)),
('result_stdout', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('result_traceback', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('celery_task_id', self.gf('django.db.models.fields.CharField')(default='', max_length=100, blank=True)),
))
db.send_create_signal('main', ['ProjectUpdate'])
# Adding field 'Project.scm_type'
db.add_column(u'main_project', 'scm_type',
self.gf('django.db.models.fields.CharField')(default='', max_length=8, null=True, blank=True),
keep_default=False)
# Adding field 'Project.scm_url'
db.add_column(u'main_project', 'scm_url',
self.gf('django.db.models.fields.URLField')(default='', max_length=1024, null=True, blank=True),
keep_default=False)
# Adding field 'Project.scm_branch'
db.add_column(u'main_project', 'scm_branch',
self.gf('django.db.models.fields.CharField')(default='', max_length=256, null=True, blank=True),
keep_default=False)
# Adding field 'Project.scm_clean'
db.add_column(u'main_project', 'scm_clean',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Adding field 'Project.scm_username'
db.add_column(u'main_project', 'scm_username',
self.gf('django.db.models.fields.CharField')(default='', max_length=256, null=True, blank=True),
keep_default=False)
# Adding field 'Project.scm_password'
db.add_column(u'main_project', 'scm_password',
self.gf('django.db.models.fields.CharField')(default='', max_length=1024, null=True, blank=True),
keep_default=False)
# Adding field 'Project.scm_key_data'
db.add_column(u'main_project', 'scm_key_data',
self.gf('django.db.models.fields.TextField')(default='', null=True, blank=True),
keep_default=False)
# Adding field 'Project.scm_key_unlock'
db.add_column(u'main_project', 'scm_key_unlock',
self.gf('django.db.models.fields.CharField')(default='', max_length=1024, null=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting model 'ProjectUpdate'
db.delete_table(u'main_projectupdate')
# Deleting field 'Project.scm_type'
db.delete_column(u'main_project', 'scm_type')
# Deleting field 'Project.scm_url'
db.delete_column(u'main_project', 'scm_url')
# Deleting field 'Project.scm_branch'
db.delete_column(u'main_project', 'scm_branch')
# Deleting field 'Project.scm_clean'
db.delete_column(u'main_project', 'scm_clean')
# Deleting field 'Project.scm_username'
db.delete_column(u'main_project', 'scm_username')
# Deleting field 'Project.scm_password'
db.delete_column(u'main_project', 'scm_password')
# Deleting field 'Project.scm_key_data'
db.delete_column(u'main_project', 'scm_key_data')
# Deleting field 'Project.scm_key_unlock'
db.delete_column(u'main_project', 'scm_key_unlock')
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', [], {'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'}),
'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', [], {'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']"}),
'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', [], {'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'}),
'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', [], {'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'}),
'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', [], {'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'}),
'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', [], {'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']"}),
'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'}),
'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']"}),
'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', [], {'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'}),
'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', [], {'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'}),
'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', [], {'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']"}),
'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', [], {'related_name': '"{\'class\': \'project\', \'app_label\': u\'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'}),
'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'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_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_url': ('django.db.models.fields.URLField', [], {'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'},
'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'}),
'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', [], {'auto_now': 'True', 'blank': 'True'}),
'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', [], {'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'}),
'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']

View File

@@ -19,6 +19,7 @@ from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.text import slugify
# Django-JSONField # Django-JSONField
from jsonfield import JSONField from jsonfield import JSONField
@@ -29,11 +30,12 @@ from taggit.managers import TaggableManager
# Django-Celery # Django-Celery
from djcelery.models import TaskMeta from djcelery.models import TaskMeta
__all__ = ['PrimordialModel', 'Organization', 'Team', 'Project', 'Credential', __all__ = ['PrimordialModel', 'Organization', 'Team', 'Project',
'Inventory', 'Host', 'Group', 'Permission', 'JobTemplate', 'Job', 'ProjectUpdate', 'Credential', 'Inventory', 'Host', 'Group',
'JobHostSummary', 'JobEvent', 'PERM_INVENTORY_ADMIN', 'Permission', 'JobTemplate', 'Job', 'JobHostSummary', 'JobEvent',
'PERM_INVENTORY_READ', 'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ',
'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_CHECK'] 'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY',
'PERM_INVENTORY_CHECK', 'JOB_STATUS_CHOICES']
logger = logging.getLogger('awx.main.models') logger = logging.getLogger('awx.main.models')
@@ -59,6 +61,16 @@ PERMISSION_TYPE_CHOICES = [
(PERM_INVENTORY_CHECK, _('Deploy To Inventory (Dry Run)')), (PERM_INVENTORY_CHECK, _('Deploy To Inventory (Dry Run)')),
] ]
JOB_STATUS_CHOICES = [
('new', _('New')), # Job has been created, but not started.
('pending', _('Pending')), # Job has been queued, but is not yet running.
('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.
]
class PrimordialModel(models.Model): class PrimordialModel(models.Model):
''' '''
common model for all object types that have these standard fields common model for all object types that have these standard fields
@@ -470,6 +482,13 @@ class Project(CommonModel):
A project represents a playbook git repo that can access a set of inventories A project represents a playbook git repo that can access a set of inventories
''' '''
SCM_TYPE_CHOICES = [
('', _('Manual')),
('git', _('Git')),
('hg', _('Mercurial')),
('svn', _('Subversion')),
]
# this is not part of the project, but managed with perms # this is not part of the project, but managed with perms
# inventories = models.ManyToManyField('Inventory', blank=True, related_name='projects') # inventories = models.ManyToManyField('Inventory', blank=True, related_name='projects')
@@ -483,7 +502,7 @@ class Project(CommonModel):
if os.path.exists(settings.PROJECTS_ROOT): if os.path.exists(settings.PROJECTS_ROOT):
paths = [x for x in os.listdir(settings.PROJECTS_ROOT) paths = [x for x in os.listdir(settings.PROJECTS_ROOT)
if os.path.isdir(os.path.join(settings.PROJECTS_ROOT, x)) if os.path.isdir(os.path.join(settings.PROJECTS_ROOT, x))
and not x.startswith('.')] and not x.startswith('.') and not x.startswith('_')]
qs = Project.objects.filter(active=True) qs = Project.objects.filter(active=True)
used_paths = qs.values_list('local_path', flat=True) used_paths = qs.values_list('local_path', flat=True)
return [x for x in paths if x not in used_paths] return [x for x in paths if x not in used_paths]
@@ -495,20 +514,97 @@ class Project(CommonModel):
# Not unique for now, otherwise "deletes" won't allow reusing the # Not unique for now, otherwise "deletes" won't allow reusing the
# same path for another active project. # same path for another active project.
#unique=True, #unique=True,
blank=True,
help_text=_('Local path (relative to PROJECTS_ROOT) containing ' help_text=_('Local path (relative to PROJECTS_ROOT) containing '
'playbooks and related files for this project.') 'playbooks and related files for this project.')
) )
#scm_type = models.CharField(max_length=64) scm_type = models.CharField(
#default_playbook = models.CharField(max_length=1024) max_length=8,
choices=SCM_TYPE_CHOICES,
blank=True,
null=True,
default='',
verbose_name=_('SCM Type'),
)
scm_url = models.URLField(
max_length=1024,
blank=True,
null=True,
default='',
verbose_name=_('SCM URL'),
)
scm_branch = models.CharField(
max_length=256,
blank=True,
null=True,
default='',
verbose_name=_('SCM Branch'),
help_text=_('Specific branch, tag or commit to checkout.'),
)
scm_clean = models.BooleanField(
default=False,
)
scm_username = models.CharField(
blank=True,
null=True,
default='',
max_length=256,
verbose_name=_('Username'),
help_text=_('SCM username for this project.'),
)
scm_password = models.CharField(
blank=True,
null=True,
default='',
max_length=1024,
verbose_name=_('Password'),
help_text=_('SCM password (or "ASK" to prompt the user).'),
)
scm_key_data = models.TextField(
blank=True,
null=True,
default='',
verbose_name=_('SSH private key'),
help_text=_('RSA or DSA private key to be used instead of password.'),
)
scm_key_unlock = models.CharField(
max_length=1024,
null=True,
blank=True,
default='',
verbose_name=_('SSH key unlock'),
help_text=_('Passphrase to unlock SSH private key if encrypted (or '
'"ASK" to prompt the user).'),
)
def save(self, *args, **kwargs):
super(Project, self).save(*args, **kwargs)
if self.scm_type and not self.local_path.startswith('_'):
slug_name = slugify(unicode(self.name)).replace(u'-', u'_')
self.local_path = u'_%d__%s' % (self.pk, slug_name)
self.save(update_fields=['local_path'])
def update(self):
if self.scm_type:
project_update = self.project_updates.create()
project_update.start()
return project_update
@property
def last_update(self):
try:
return self.project_updates.order_by('-modified')[0]
except IndexError:
pass
def get_absolute_url(self): def get_absolute_url(self):
return reverse('main:project_detail', args=(self.pk,)) return reverse('main:project_detail', args=(self.pk,))
def get_project_path(self): def get_project_path(self, check_if_exists=True):
local_path = os.path.basename(self.local_path) local_path = os.path.basename(self.local_path)
if local_path and not local_path.startswith('.'): if local_path and not local_path.startswith('.'):
proj_path = os.path.join(settings.PROJECTS_ROOT, local_path) proj_path = os.path.join(settings.PROJECTS_ROOT, local_path)
if os.path.exists(proj_path): if not check_if_exists or os.path.exists(proj_path):
return proj_path return proj_path
@property @property
@@ -543,6 +639,118 @@ class Project(CommonModel):
results.append(playbook) results.append(playbook)
return results return results
class ProjectUpdate(models.Model):
'''
Job for tracking internal project updates.
'''
class Meta:
app_label = 'main'
created = models.DateTimeField(
auto_now_add=True,
)
modified = models.DateTimeField(
auto_now=True,
)
project = models.ForeignKey(
'Project',
related_name='project_updates',
on_delete=models.CASCADE,
editable=False,
)
cancel_flag = models.BooleanField(
blank=True,
default=False,
editable=False,
)
status = models.CharField(
max_length=20,
choices=JOB_STATUS_CHOICES,
default='new',
editable=False,
)
failed = models.BooleanField(
default=False,
editable=False,
)
job_args = models.CharField(
max_length=1024,
blank=True,
default='',
editable=False,
)
job_cwd = models.CharField(
max_length=1024,
blank=True,
default='',
editable=False,
)
job_env = JSONField(
blank=True,
default={},
editable=False,
)
result_stdout = models.TextField(
blank=True,
default='',
editable=False,
)
result_traceback = models.TextField(
blank=True,
default='',
editable=False,
)
celery_task_id = models.CharField(
max_length=100,
blank=True,
default='',
editable=False,
)
def save(self, *args, **kwargs):
self.failed = bool(self.status in ('failed', 'error', 'canceled'))
super(ProjectUpdate, self).save(*args, **kwargs)
@property
def celery_task(self):
try:
if self.celery_task_id:
return TaskMeta.objects.get(task_id=self.celery_task_id)
except TaskMeta.DoesNotExist:
pass
@property
def can_start(self):
return bool(self.status == 'new')
def start(self, **kwargs):
from awx.main.tasks import RunProjectUpdate
if not self.can_start:
return False
self.status = 'pending'
self.save(update_fields=['status'])
task_result = RunProjectUpdate().delay(self.pk, **kwargs)
# Reload project update from database so we don't clobber results
# from RunProjectUpdate (mainly from tests when using Django 1.4.x).
project_update = ProjectUpdate.objects.get(pk=self.pk)
# The TaskMeta instance in the database isn't created until the worker
# starts processing the task, so we can only store the task ID here.
project_update.celery_task_id = task_result.task_id
project_update.save(update_fields=['celery_task_id'])
return True
@property
def can_cancel(self):
return bool(self.status in ('pending', 'running'))
def cancel(self):
if self.can_cancel:
if not self.cancel_flag:
self.cancel_flag = True
self.save(update_fields=['cancel_flag'])
return self.cancel_flag
class Permission(CommonModelNameNotUnique): class Permission(CommonModelNameNotUnique):
''' '''
A permission allows a user, project, or team to be able to use an inventory source. A permission allows a user, project, or team to be able to use an inventory source.
@@ -702,16 +910,6 @@ class Job(CommonModelNameNotUnique):
('scheduled', _('Scheduled')), ('scheduled', _('Scheduled')),
] ]
STATUS_CHOICES = [
('new', _('New')), # Job has been created, but not started.
('pending', _('Pending')), # Job has been queued, but is not yet running.
('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.
]
class Meta: class Meta:
app_label = 'main' app_label = 'main'
@@ -783,7 +981,7 @@ class Job(CommonModelNameNotUnique):
) )
status = models.CharField( status = models.CharField(
max_length=20, max_length=20,
choices=STATUS_CHOICES, choices=JOB_STATUS_CHOICES,
default='new', default='new',
editable=False, editable=False,
) )

View File

@@ -195,7 +195,7 @@ class ProjectSerializer(BaseSerializer):
valid_local_paths = Project.get_local_path_choices() valid_local_paths = Project.get_local_path_choices()
if self.object: if self.object:
valid_local_paths.append(self.object.local_path) valid_local_paths.append(self.object.local_path)
if attrs[source] not in valid_local_paths: if source in attrs and attrs[source] not in valid_local_paths:
raise serializers.ValidationError('Invalid path choice') raise serializers.ValidationError('Invalid path choice')
return attrs return attrs

View File

@@ -8,6 +8,7 @@ import logging
import os import os
import subprocess import subprocess
import tempfile import tempfile
import time
import traceback import traceback
# Pexpect # Pexpect
@@ -20,33 +21,31 @@ from celery import Task
from django.conf import settings from django.conf import settings
# AWX # AWX
from awx.main.models import Job from awx.main.models import Job, ProjectUpdate
__all__ = ['RunJob'] __all__ = ['RunJob', 'RunProjectUpdate']
logger = logging.getLogger('awx.main.tasks') logger = logging.getLogger('awx.main.tasks')
class RunJob(Task): class BaseTask(Task):
'''
Celery task to run a job using ansible-playbook. name = None
''' model = None
name = 'run_job' def update_model(self, pk, **updates):
def update_job(self, job_pk, **job_updates):
''' '''
Reload Job from database and update the given fields. Reload model from database and update the given fields.
''' '''
job = Job.objects.get(pk=job_pk) instance = self.model.objects.get(pk=pk)
if job_updates: if updates:
update_fields = [] update_fields = []
for field, value in job_updates.items(): for field, value in updates.items():
setattr(job, field, value) setattr(instance, field, value)
update_fields.append(field) update_fields.append(field)
if field == 'status': if field == 'status':
update_fields.append('failed') update_fields.append('failed')
job.save(update_fields=update_fields) instance.save(update_fields=update_fields)
return job return instance
def get_path_to(self, *args): def get_path_to(self, *args):
''' '''
@@ -54,21 +53,141 @@ class RunJob(Task):
''' '''
return os.path.abspath(os.path.join(os.path.dirname(__file__), *args)) return os.path.abspath(os.path.join(os.path.dirname(__file__), *args))
def build_ssh_key_path(self, job, **kwargs): def build_ssh_key_path(self, instance, **kwargs):
''' '''
Create a temporary file containing the SSH private key. Create a temporary file containing the SSH private key.
''' '''
creds = job.credential ssh_key_data = getattr(instance, 'ssh_key_data', '')
if creds and creds.ssh_key_data: if not ssh_key_data:
credential = getattr(instance, 'credential', None)
ssh_key_data = getattr(credential, 'ssh_key_data', '')
if ssh_key_data:
# FIXME: File permissions? # FIXME: File permissions?
handle, path = tempfile.mkstemp() handle, path = tempfile.mkstemp()
f = os.fdopen(handle, 'w') f = os.fdopen(handle, 'w')
f.write(creds.ssh_key_data) f.write(ssh_key_data)
f.close() f.close()
return path return path
else: else:
return '' return ''
def build_passwords(self, instance, **kwargs):
'''
Build a dictionary of passwords responding to prompts.
'''
return {}
def build_env(self, instance, **kwargs):
'''
Build environment dictionary for ansible-playbook.
'''
env = dict(os.environ.items())
# Add ANSIBLE_* settings to the subprocess environment.
for attr in dir(settings):
if attr == attr.upper() and attr.startswith('ANSIBLE_'):
env[attr] = str(getattr(settings, attr))
# Also set environment variables configured in AWX_TASK_ENV setting.
for key, value in settings.AWX_TASK_ENV.items():
env[key] = str(value)
# Set environment variables needed for inventory and job event
# callbacks to work.
env['ANSIBLE_NOCOLOR'] = '1' # Prevent output of escape sequences.
return env
def build_args(self, instance, **kwargs):
raise NotImplementedError
def build_cwd(self, instance, **kwargs):
raise NotImplementedError
def get_password_prompts(self):
'''
Return a dictionary of prompt regular expressions and password lookup
keys.
'''
return {
r'Enter passphrase for .*:': 'ssh_key_unlock',
r'Bad passphrase, try again for .*:': '',
}
def run_pexpect(self, pk, args, cwd, env, passwords):
'''
Run the given command using pexpect to capture output and provide
passwords when requested.
'''
status, stdout = 'error', ''
logfile = cStringIO.StringIO()
logfile_pos = logfile.tell()
child = pexpect.spawn(args[0], args[1:], cwd=cwd, env=env)
child.logfile_read = logfile
canceled = False
last_stdout_update = time.time()
expect_list = []
expect_passwords = {}
for n, item in enumerate(self.get_password_prompts().items()):
expect_list.append(item[0])
expect_passwords[n] = passwords.get(item[1], '')
expect_list.extend([pexpect.TIMEOUT, pexpect.EOF])
while child.isalive():
result_id = child.expect(expect_list, timeout=2)
if result_id in expect_passwords:
child.sendline(expect_passwords[result_id])
updates = {}
if logfile_pos != logfile.tell():
updates['result_stdout'] = logfile.getvalue()
last_stdout_update = time.time()
instance = self.update_model(pk, **updates)
if instance.cancel_flag:
child.close(True)
canceled = True
#elif (time.time() - last_stdout_update) > 30: # FIXME: Configurable idle timeout?
# print 'canceling...'
# child.close(True)
# canceled = True
if canceled:
status = 'canceled'
elif child.exitstatus == 0:
status = 'successful'
else:
status = 'failed'
stdout = logfile.getvalue()
return status, stdout
def run(self, pk, **kwargs):
'''
Run the job/task using ansible-playbook and capture its output.
'''
instance = self.update_model(pk, status='running')
status, stdout, tb = 'error', '', ''
try:
kwargs['ssh_key_path'] = self.build_ssh_key_path(instance, **kwargs)
kwargs['passwords'] = self.build_passwords(instance, **kwargs)
args = self.build_args(instance, **kwargs)
cwd = self.build_cwd(instance, **kwargs)
env = self.build_env(instance, **kwargs)
instance = self.update_model(pk, job_args=json.dumps(args),
job_cwd=cwd, job_env=env)
status, stdout = self.run_pexpect(pk, args, cwd, env,
kwargs['passwords'])
except Exception:
tb = traceback.format_exc()
finally:
if kwargs.get('ssh_key_path', ''):
try:
os.remove(kwargs['ssh_key_path'])
except IOError:
pass
self.update_model(pk, status=status, result_stdout=stdout,
result_traceback=tb)
class RunJob(BaseTask):
'''
Celery task to run a job using ansible-playbook.
'''
name = 'run_job'
model = Job
def build_passwords(self, job, **kwargs): def build_passwords(self, job, **kwargs):
''' '''
Build a dictionary of passwords for SSH private key, SSH user and sudo. Build a dictionary of passwords for SSH private key, SSH user and sudo.
@@ -87,22 +206,12 @@ class RunJob(Task):
Build environment dictionary for ansible-playbook. Build environment dictionary for ansible-playbook.
''' '''
plugin_dir = self.get_path_to('..', 'plugins', 'callback') plugin_dir = self.get_path_to('..', 'plugins', 'callback')
env = dict(os.environ.items()) env = super(RunJob, self).build_env(job, **kwargs)
# question: when running over CLI, generate a random ID or grab next, etc?
# answer: TBD
# Add ANSIBLE_* settings to the subprocess environment.
for attr in dir(settings):
if attr == attr.upper() and attr.startswith('ANSIBLE_'):
env[attr] = str(getattr(settings, attr))
# Also set environment variables configured in AWX_TASK_ENV setting.
for key, value in settings.AWX_TASK_ENV.items():
env[key] = str(value)
# Set environment variables needed for inventory and job event # Set environment variables needed for inventory and job event
# callbacks to work. # callbacks to work.
env['JOB_ID'] = str(job.pk) env['JOB_ID'] = str(job.pk)
env['INVENTORY_ID'] = str(job.inventory.pk) env['INVENTORY_ID'] = str(job.inventory.pk)
env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir
env['ANSIBLE_NOCOLOR'] = '1' # Prevent output of escape sequences.
env['REST_API_URL'] = settings.INTERNAL_API_URL env['REST_API_URL'] = settings.INTERNAL_API_URL
env['REST_API_TOKEN'] = job.task_auth_token or '' env['REST_API_TOKEN'] = job.task_auth_token or ''
return env return env
@@ -154,79 +263,88 @@ class RunJob(Task):
args = ['ssh-agent', 'sh', '-c', cmd] args = ['ssh-agent', 'sh', '-c', cmd]
return args return args
def run_pexpect(self, job_pk, args, cwd, env, passwords): def build_cwd(self, job, **kwargs):
''' cwd = job.project.get_project_path()
Run the job using pexpect to capture output and provide passwords when if not cwd:
requested.
'''
status, stdout = 'error', ''
logfile = cStringIO.StringIO()
logfile_pos = logfile.tell()
child = pexpect.spawn(args[0], args[1:], cwd=cwd, env=env)
child.logfile_read = logfile
job_canceled = False
while child.isalive():
expect_list = [
r'Enter passphrase for .*:',
r'Bad passphrase, try again for .*:',
r'sudo password.*:',
r'SSH password:',
r'Password:',
pexpect.TIMEOUT,
pexpect.EOF,
]
result_id = child.expect(expect_list, timeout=2)
if result_id == 0:
child.sendline(passwords.get('ssh_key_unlock', ''))
elif result_id == 1:
child.sendline('')
elif result_id == 2:
child.sendline(passwords.get('sudo_password', ''))
elif result_id in (3, 4):
child.sendline(passwords.get('ssh_password', ''))
job_updates = {}
if logfile_pos != logfile.tell():
job_updates['result_stdout'] = logfile.getvalue()
job = self.update_job(job_pk, **job_updates)
if job.cancel_flag:
child.close(True)
job_canceled = True
if job_canceled:
status = 'canceled'
elif child.exitstatus == 0:
status = 'successful'
else:
status = 'failed'
stdout = logfile.getvalue()
return status, stdout
def run(self, job_pk, **kwargs):
'''
Run the job using ansible-playbook and capture its output.
'''
job = self.update_job(job_pk, status='running')
status, stdout, tb = 'error', '', ''
try:
kwargs['ssh_key_path'] = self.build_ssh_key_path(job, **kwargs)
kwargs['passwords'] = self.build_passwords(job, **kwargs)
args = self.build_args(job, **kwargs)
cwd = job.project.get_project_path()
root = settings.PROJECTS_ROOT root = settings.PROJECTS_ROOT
if not cwd: raise RuntimeError('project local_path %s cannot be found in %s' %
raise RuntimeError('project local_path %s cannot be found in %s' % (job.project.local_path, root))
(job.project.local_path, root)) return cwd
env = self.build_env(job, **kwargs)
job = self.update_job(job_pk, job_args=json.dumps(args), def get_password_prompts(self):
job_cwd=cwd, job_env=env) d = super(RunJob, self).get_password_prompts()
status, stdout = self.run_pexpect(job_pk, args, cwd, env, d.update({
kwargs['passwords']) r'sudo password.*:': 'sudo_password',
except Exception: r'SSH password:': 'ssh_password',
tb = traceback.format_exc() r'Password:': 'ssh_password',
finally: })
if kwargs.get('ssh_key_path', ''): return d
try:
os.remove(kwargs['ssh_key_path']) class RunProjectUpdate(BaseTask):
except IOError:
pass name = 'run_project_update'
self.update_job(job_pk, status=status, result_stdout=stdout, model = ProjectUpdate
result_traceback=tb)
def build_passwords(self, project_update, **kwargs):
'''
Build a dictionary of passwords for SSH private key.
'''
passwords = {}
project = project_update.project
value = project.scm_key_unlock
if value not in ('', 'ASK'):
passwords['ssh_key_unlock'] = value
passwords['scm_username'] = project.scm_username
passwords['scm_password'] = project.scm_password
return passwords
def build_env(self, project_update, **kwargs):
'''
Build environment dictionary for ansible-playbook.
'''
env = super(RunProjectUpdate, self).build_env(project_update, **kwargs)
return env
def build_args(self, project_update, **kwargs):
'''
Build command line argument list for running ansible-playbook,
optionally using ssh-agent for public/private key authentication.
'''
args = ['ansible-playbook', '-i', 'localhost,']
args.append('-%s' % ('v' * 3))
# FIXME
project = project_update.project
extra_vars = {
'project_path': project.get_project_path(check_if_exists=False),
'scm_type': project.scm_type,
'scm_url': project.scm_url,
'scm_branch': project.scm_branch or 'HEAD',
'scm_clean': project.scm_clean,
'scm_username': project.scm_username,
'scm_password': project.scm_password,
}
args.extend(['-e', json.dumps(extra_vars)])
args.append('project_update.yml')
ssh_key_path = kwargs.get('ssh_key_path', '')
subcmds = [
('ssh-add', '-D'),
args,
]
if ssh_key_path:
subcmds.insert(1, ('ssh-add', ssh_key_path))
cmd = ' && '.join([subprocess.list2cmdline(x) for x in subcmds])
args = ['ssh-agent', 'sh', '-c', cmd]
return args
def build_cwd(self, project_update, **kwargs):
return self.get_path_to('..', 'playbooks')
def get_password_prompts(self):
d = super(RunProjectUpdate, self).get_password_prompts()
d.update({
r'Username for.*:': 'scm_username',
r'Password for.*:': 'scm_password',
r'Are you sure you want to continue connecting (yes/no)\?': 'yes',
})
return d

View File

@@ -4,7 +4,7 @@
from awx.main.tests.organizations import OrganizationsTest from awx.main.tests.organizations import OrganizationsTest
from awx.main.tests.users import UsersTest from awx.main.tests.users import UsersTest
from awx.main.tests.inventory import InventoryTest from awx.main.tests.inventory import InventoryTest
from awx.main.tests.projects import ProjectsTest from awx.main.tests.projects import ProjectsTest, ProjectUpdatesTest
from awx.main.tests.commands import * from awx.main.tests.commands import *
from awx.main.tests.scripts import * from awx.main.tests.scripts import *
from awx.main.tests.tasks import RunJobTest from awx.main.tests.tasks import RunJobTest

View File

@@ -777,7 +777,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
# Sue can start a job (when passwords are already saved) as long as the # Sue can start a job (when passwords are already saved) as long as the
# status is new. Reverse list so "new" will be last. # status is new. Reverse list so "new" will be last.
for status in reversed([x[0] for x in Job.STATUS_CHOICES]): for status in reversed([x[0] for x in JOB_STATUS_CHOICES]):
job.status = status job.status = status
job.save() job.save()
with self.current_user(self.user_sue): with self.current_user(self.user_sue):
@@ -863,7 +863,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
self.check_invalid_auth(url, methods=('post',)) self.check_invalid_auth(url, methods=('post',))
# sue can cancel the job, but only when it is pending or running. # sue can cancel the job, but only when it is pending or running.
for status in [x[0] for x in Job.STATUS_CHOICES]: for status in [x[0] for x in JOB_STATUS_CHOICES]:
job.status = status job.status = status
job.save() job.save()
with self.current_user(self.user_sue): with self.current_user(self.user_sue):

View File

@@ -1,19 +1,24 @@
# Copyright (c) 2013 AnsibleWorks, Inc. # Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved. # All Rights Reserved.
# Python
import datetime import datetime
import json import json
import os import os
import tempfile import tempfile
import urlparse
# Django
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
import django.test import django.test
from django.test.client import Client from django.test.client import Client
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test.utils import override_settings
# AWX
from awx.main.models import * from awx.main.models import *
from awx.main.tests.base import BaseTest from awx.main.tests.base import BaseTest, BaseTransactionTest
TEST_PLAYBOOK = '''- hosts: mygroup TEST_PLAYBOOK = '''- hosts: mygroup
gather_facts: false gather_facts: false
@@ -607,3 +612,170 @@ class ProjectsTest(BaseTest):
self.delete(url2, expect=403, auth=self.get_other_credentials()) self.delete(url2, expect=403, auth=self.get_other_credentials())
self.delete(url2, expect=204, auth=self.get_super_credentials()) self.delete(url2, expect=204, auth=self.get_super_credentials())
self.delete(url2, expect=404, auth=self.get_other_credentials()) self.delete(url2, expect=404, auth=self.get_other_credentials())
@override_settings(CELERY_ALWAYS_EAGER=True,
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
ANSIBLE_TRANSPORT='local')
class ProjectUpdatesTest(BaseTransactionTest):
def setUp(self):
super(ProjectUpdatesTest, self).setUp()
self.setup_users()
self.skipTest('blah')
def create_project(self, **kwargs):
project = Project.objects.create(**kwargs)
project_path = project.get_project_path(check_if_exists=False)
self._temp_project_dirs.append(project_path)
return project
def update_url_auth(self, url, username=None, password=None):
parts = urlparse.urlsplit(url)
def check_project_update(self, project, should_fail=False):
print project.local_path
pu = project.update()
self.assertTrue(pu)
pu = ProjectUpdate.objects.get(pk=pu.pk)
print pu.status
if should_fail:
self.assertEqual(pu.status, 'failed', pu.result_stdout)
else:
self.assertEqual(pu.status, 'successful', pu.result_stdout)
#print pu.result_traceback
#print pu.result_stdout
#print
def change_file_in_project(self, project):
project_path = project.get_project_path()
self.assertTrue(project_path)
for root, dirs, files in os.walk(project_path):
for f in files:
if f.startswith('.'):
continue
path = os.path.join(root, f)
file(path, 'wb').write('CHANGED FILE')
return
self.fail('no file found to change!')
def check_project_scm(self, project):
# Initial checkout.
self.check_project_update(project)
# Update to existing checkout.
self.check_project_update(project)
# Change file then update (with scm_clean=False).
self.assertFalse(project.scm_clean)
self.change_file_in_project(project)
self.check_project_update(project, should_fail=True)
# Set scm_clean=True then try to update again.
project.scm_clean = True
project.save()
self.check_project_update(project)
def test_public_git_project_over_https(self):
scm_url = getattr(settings, 'TEST_GIT_PUBLIC_HTTPS',
'https://github.com/ansible/ansible.github.com.git')
if not all([scm_url]):
self.skipTest('no public git repo defined for https!')
project = self.create_project(
name='my public git project over https',
scm_type='git',
scm_url=scm_url,
)
self.check_project_scm(project)
def test_private_git_project_over_https(self):
scm_url = getattr(settings, 'TEST_GIT_PRIVATE_HTTPS', '')
scm_username = getattr(settings, 'TEST_GIT_USERNAME', '')
scm_password = getattr(settings, 'TEST_GIT_PASSWORD', '')
if not all([scm_url, scm_username, scm_password]):
self.skipTest('no private git repo defined for https!')
project = self.create_project(
name='my private git project over https',
scm_type='git',
scm_url=scm_url,
scm_username=scm_username,
scm_password=scm_password,
)
self.check_project_scm(project)
def test_private_git_project_over_ssh(self):
scm_url = getattr(settings, 'TEST_GIT_PRIVATE_SSH', '')
scm_key_data = getattr(settings, 'TEST_GIT_KEY_DATA', '')
if not all([scm_url, scm_key_data]):
self.skipTest('no private git repo defined for ssh!')
project = self.create_project(
name='my private git project over ssh',
scm_type='git',
scm_url=scm_url,
scm_key_data=scm_key_data,
)
self.check_project_scm(project)
def test_public_hg_project_over_https(self):
scm_url = getattr(settings, 'TEST_HG_PUBLIC_HTTPS',
'https://bitbucket.org/cchurch/django-hotrunner')
if not all([scm_url]):
self.skipTest('no public hg repo defined for https!')
project = self.create_project(
name='my public hg project over https',
scm_type='hg',
scm_url=scm_url,
)
self.check_project_scm(project)
def test_private_hg_project_over_https(self):
scm_url = getattr(settings, 'TEST_HG_PRIVATE_HTTPS', '')
scm_username = getattr(settings, 'TEST_HG_USERNAME', '')
scm_password = getattr(settings, 'TEST_HG_PASSWORD', '')
if not all([scm_url, scm_username, scm_password]):
self.skipTest('no private hg repo defined for https!')
project = self.create_project(
name='my private hg project over https',
scm_type='hg',
scm_url=scm_url,
scm_username=scm_username,
scm_password=scm_password,
)
self.check_project_scm(project)
def test_private_hg_project_over_ssh(self):
scm_url = getattr(settings, 'TEST_HG_PRIVATE_SSH', '')
scm_key_data = getattr(settings, 'TEST_HG_KEY_DATA', '')
if not all([scm_url, scm_key_data]):
self.skipTest('no private hg repo defined for ssh!')
project = self.create_project(
name='my private hg project over ssh',
scm_type='hg',
scm_url=scm_url,
scm_key_data=scm_key_data,
)
self.check_project_scm(project)
def test_public_svn_project_over_https(self):
scm_url = getattr(settings, 'TEST_SVN_PUBLIC_HTTPS',
'https://projects.ninemoreminutes.com/svn/django-site-utils/')
if not all([scm_url]):
self.skipTest('no public svn repo defined for https!')
project = self.create_project(
name='my public svn project over https',
scm_type='svn',
scm_url=scm_url,
)
self.check_project_scm(project)
def test_private_svn_project_over_https(self):
scm_url = getattr(settings, 'TEST_SVN_PRIVATE_HTTPS', '')
scm_username = getattr(settings, 'TEST_SVN_USERNAME', '')
scm_password = getattr(settings, 'TEST_SVN_PASSWORD', '')
if not all([scm_url, scm_username, scm_password]):
self.skipTest('no private svn repo defined for https!')
project = self.create_project(
name='my private svn project over https',
scm_type='svn',
scm_url=scm_url,
scm_username=scm_username,
scm_password=scm_password,
)
self.check_project_scm(project)

View File

@@ -0,0 +1,37 @@
---
# The following variables will be set by the runner of this playbook:
# project_path: PROJECTS_DIR/_local_path_
# scm_type: git|hg|svn
# scm_url: https://server/repo
# scm_branch: HEAD
# scm_clean: true/false
- hosts: all
connection: local
gather_facts: false
tasks:
# Git Tasks
- 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
tags: git
# Mercurial Tasks
- name: update project using hg
hg: dest={{project_path}} repo={{scm_url}} version={{scm_branch}} force={{scm_clean}}
when: scm_type == 'hg'
async: 0
poll: 5
tags: hg
# Subversion Tasks
- 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
tags: svn

View File

@@ -115,3 +115,29 @@ LOGGING['handlers']['syslog'] = {
# the celery task. # the celery task.
#AWX_TASK_ENV['FOO'] = 'BAR' #AWX_TASK_ENV['FOO'] = 'BAR'
# Define these variables to enable more complete testing of project support for
# SCM updates.
try:
path = os.path.expanduser(os.path.expandvars('~/.ssh/id_rsa.pub'))
TEST_SSH_KEY_DATA = file(path, 'rb').read()
except OSError:
TEST_SSH_KEY_DATA = ''
TEST_GIT_USERNAME = ''
TEST_GIT_PASSWORD = ''
TEST_GIT_KEY_DATA = TEST_SSH_KEY_DATA
TEST_GIT_PUBLIC_HTTPS = 'https://github.com/ansible/ansible-examples.git'
TEST_GIT_PRIVATE_HTTPS = 'https://github.com/ansible/ansible-doc.git'
TEST_GIT_PRIVATE_SSH = 'git@github.com:ansible/ansible-doc.git'
TEST_HG_USERNAME = ''
TEST_HG_PASSWORD = ''
TEST_HG_KEY_DATA = TEST_SSH_KEY_DATA
TEST_HG_PUBLIC_HTTPS = 'https://bitbucket.org/cchurch/django-hotrunner'
TEST_HG_PRIVATE_HTTPS = ''
TEST_HG_PRIVATE_SSH = ''
TEST_SVN_USERNAME = ''
TEST_SVN_PASSWORD = ''
TEST_SVN_PUBLIC_HTTPS = 'https://projects.ninemoreminutes.com/svn/django-site-utils/trunk/'
TEST_SVN_PRIVATE_HTTPS = ''