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')
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):
list_display = ('name', 'description', 'active')
fieldsets = (
(None, {'fields': (('name', 'active'), 'description', 'local_path',
'get_playbooks_display')}),
(_('SCM'), {'fields': ('scm_type', 'scm_url', 'scm_branch')}),
(_('Tags'), {'fields': ('tags',)}),
(_('Audit'), {'fields': ('created', 'created_by',)}),
)
readonly_fields = ('created', 'created_by', 'get_playbooks_display')
form = ProjectAdminForm
inlines = [ProjectUpdateInline]
def get_playbooks_display(self, obj):
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.contrib.auth.models import User
from django.utils.timezone import now
from django.utils.text import slugify
# Django-JSONField
from jsonfield import JSONField
@ -29,11 +30,12 @@ from taggit.managers import TaggableManager
# Django-Celery
from djcelery.models import TaskMeta
__all__ = ['PrimordialModel', 'Organization', 'Team', 'Project', 'Credential',
'Inventory', 'Host', 'Group', 'Permission', 'JobTemplate', 'Job',
'JobHostSummary', 'JobEvent', 'PERM_INVENTORY_ADMIN',
'PERM_INVENTORY_READ', 'PERM_INVENTORY_WRITE',
'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_CHECK']
__all__ = ['PrimordialModel', 'Organization', 'Team', 'Project',
'ProjectUpdate', 'Credential', 'Inventory', 'Host', 'Group',
'Permission', 'JobTemplate', 'Job', 'JobHostSummary', 'JobEvent',
'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ',
'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY',
'PERM_INVENTORY_CHECK', 'JOB_STATUS_CHOICES']
logger = logging.getLogger('awx.main.models')
@ -59,6 +61,16 @@ PERMISSION_TYPE_CHOICES = [
(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):
'''
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
'''
SCM_TYPE_CHOICES = [
('', _('Manual')),
('git', _('Git')),
('hg', _('Mercurial')),
('svn', _('Subversion')),
]
# this is not part of the project, but managed with perms
# inventories = models.ManyToManyField('Inventory', blank=True, related_name='projects')
@ -483,7 +502,7 @@ class Project(CommonModel):
if os.path.exists(settings.PROJECTS_ROOT):
paths = [x for x in os.listdir(settings.PROJECTS_ROOT)
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)
used_paths = qs.values_list('local_path', flat=True)
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
# 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.')
)
#scm_type = models.CharField(max_length=64)
#default_playbook = models.CharField(max_length=1024)
scm_type = models.CharField(
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):
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)
if local_path and not local_path.startswith('.'):
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
@property
@ -543,6 +639,118 @@ class Project(CommonModel):
results.append(playbook)
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):
'''
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')),
]
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:
app_label = 'main'
@ -783,7 +981,7 @@ class Job(CommonModelNameNotUnique):
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
choices=JOB_STATUS_CHOICES,
default='new',
editable=False,
)

View File

@ -195,7 +195,7 @@ class ProjectSerializer(BaseSerializer):
valid_local_paths = Project.get_local_path_choices()
if self.object:
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')
return attrs

View File

@ -8,6 +8,7 @@ import logging
import os
import subprocess
import tempfile
import time
import traceback
# Pexpect
@ -20,33 +21,31 @@ from celery import Task
from django.conf import settings
# 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')
class RunJob(Task):
'''
Celery task to run a job using ansible-playbook.
'''
class BaseTask(Task):
name = None
model = None
name = 'run_job'
def update_job(self, job_pk, **job_updates):
def update_model(self, pk, **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)
if job_updates:
instance = self.model.objects.get(pk=pk)
if updates:
update_fields = []
for field, value in job_updates.items():
setattr(job, field, value)
for field, value in updates.items():
setattr(instance, field, value)
update_fields.append(field)
if field == 'status':
update_fields.append('failed')
job.save(update_fields=update_fields)
return job
instance.save(update_fields=update_fields)
return instance
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))
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.
'''
creds = job.credential
if creds and creds.ssh_key_data:
ssh_key_data = getattr(instance, '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?
handle, path = tempfile.mkstemp()
f = os.fdopen(handle, 'w')
f.write(creds.ssh_key_data)
f.write(ssh_key_data)
f.close()
return path
else:
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):
'''
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.
'''
plugin_dir = self.get_path_to('..', 'plugins', 'callback')
env = dict(os.environ.items())
# 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)
env = super(RunJob, self).build_env(job, **kwargs)
# Set environment variables needed for inventory and job event
# callbacks to work.
env['JOB_ID'] = str(job.pk)
env['INVENTORY_ID'] = str(job.inventory.pk)
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_TOKEN'] = job.task_auth_token or ''
return env
@ -154,79 +263,88 @@ class RunJob(Task):
args = ['ssh-agent', 'sh', '-c', cmd]
return args
def run_pexpect(self, job_pk, args, cwd, env, passwords):
'''
Run the job 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
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()
def build_cwd(self, job, **kwargs):
cwd = job.project.get_project_path()
if not cwd:
root = settings.PROJECTS_ROOT
if not cwd:
raise RuntimeError('project local_path %s cannot be found in %s' %
(job.project.local_path, root))
env = self.build_env(job, **kwargs)
job = self.update_job(job_pk, job_args=json.dumps(args),
job_cwd=cwd, job_env=env)
status, stdout = self.run_pexpect(job_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_job(job_pk, status=status, result_stdout=stdout,
result_traceback=tb)
raise RuntimeError('project local_path %s cannot be found in %s' %
(job.project.local_path, root))
return cwd
def get_password_prompts(self):
d = super(RunJob, self).get_password_prompts()
d.update({
r'sudo password.*:': 'sudo_password',
r'SSH password:': 'ssh_password',
r'Password:': 'ssh_password',
})
return d
class RunProjectUpdate(BaseTask):
name = 'run_project_update'
model = ProjectUpdate
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.users import UsersTest
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.scripts import *
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
# 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.save()
with self.current_user(self.user_sue):
@ -863,7 +863,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
self.check_invalid_auth(url, methods=('post',))
# sue can cancel the job, but only when it is pending or running.
for status in [x[0] for x in Job.STATUS_CHOICES]:
for status in [x[0] for x in JOB_STATUS_CHOICES]:
job.status = status
job.save()
with self.current_user(self.user_sue):

View File

@ -1,19 +1,24 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
# Python
import datetime
import json
import os
import tempfile
import urlparse
# Django
from django.conf import settings
from django.contrib.auth.models import User
import django.test
from django.test.client import Client
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
# AWX
from awx.main.models import *
from awx.main.tests.base import BaseTest
from awx.main.tests.base import BaseTest, BaseTransactionTest
TEST_PLAYBOOK = '''- hosts: mygroup
gather_facts: false
@ -607,3 +612,170 @@ class ProjectsTest(BaseTest):
self.delete(url2, expect=403, auth=self.get_other_credentials())
self.delete(url2, expect=204, auth=self.get_super_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.
#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 = ''