mirror of
https://github.com/ansible/awx.git
synced 2026-04-07 02:59:21 -02:30
For AC-132. Work in progress on project SCM support.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
359
awx/main/migrations/0009_v13_changes.py
Normal file
359
awx/main/migrations/0009_v13_changes.py
Normal 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']
|
||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
37
awx/playbooks/project_update.yml
Normal file
37
awx/playbooks/project_update.yml
Normal 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
|
||||||
@@ -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 = ''
|
||||||
|
|||||||
Reference in New Issue
Block a user