More work in progress on AC-132.

This commit is contained in:
Chris Church
2013-08-26 02:28:37 -04:00
parent e594296c9b
commit ee3ba2c0e1
18 changed files with 418 additions and 61 deletions

View File

@@ -6,6 +6,7 @@ recursive-include awx/ui *.html
recursive-include awx/ui/static *.css *.ico *.png *.gif *.jpg recursive-include awx/ui/static *.css *.ico *.png *.gif *.jpg
recursive-include awx/ui/static *.eot *.svg *.ttf *.woff *.otf recursive-include awx/ui/static *.eot *.svg *.ttf *.woff *.otf
recursive-include awx/ui/static/lib * recursive-include awx/ui/static/lib *
recursive-include awx/playbooks *.yml
recursive-include awx/lib/site-packages * recursive-include awx/lib/site-packages *
recursive-include config * recursive-include config *
recursive-include config/deb * recursive-include config/deb *

View File

@@ -601,6 +601,33 @@ class ProjectAccess(BaseAccess):
def can_delete(self, obj): def can_delete(self, obj):
return self.can_change(obj, None) return self.can_change(obj, None)
class ProjectUpdateAccess(BaseAccess):
'''
I can see project updates when I can see the project.
I can change/delete when:
- I am a superuser.
- I am an admin in an organization associated with the project.
- I created it (for now?).
'''
model = ProjectUpdate
def get_queryset(self):
qs = ProjectUpdate.objects.filter(active=True).distinct()
qs = qs.select_related('created_by', 'project')
#if self.user.is_superuser:
return qs
#allowed = [PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK]
#return qs.filter(
# Q(created_by=self.user) |
# Q(organizations__admins__in=[self.user]) |
# Q(organizations__users__in=[self.user]) |
# Q(teams__users__in=[self.user]) |
# Q(permissions__user=self.user, permissions__permission_type__in=allowed) |
# Q(permissions__team__users__in=[self.user], permissions__permission_type__in=allowed)
#)
class PermissionAccess(BaseAccess): class PermissionAccess(BaseAccess):
''' '''
I can see a permission when: I can see a permission when:
@@ -944,6 +971,7 @@ register_access(Group, GroupAccess)
register_access(Credential, CredentialAccess) register_access(Credential, CredentialAccess)
register_access(Team, TeamAccess) register_access(Team, TeamAccess)
register_access(Project, ProjectAccess) register_access(Project, ProjectAccess)
register_access(ProjectUpdate, ProjectUpdateAccess)
register_access(Permission, PermissionAccess) register_access(Permission, PermissionAccess)
register_access(JobTemplate, JobTemplateAccess) register_access(JobTemplate, JobTemplateAccess)
register_access(Job, JobAccess) register_access(Job, JobAccess)

View File

@@ -34,6 +34,7 @@ class APIView(views.APIView):
def get_description_context(self): def get_description_context(self):
return { return {
'docstring': type(self).__doc__ or '', 'docstring': type(self).__doc__ or '',
'new_in_13': getattr(self, 'new_in_13', False),
} }
def get_description(self, html=False): def get_description(self, html=False):

View File

@@ -11,8 +11,10 @@ class Migration(SchemaMigration):
# Adding model 'ProjectUpdate' # Adding model 'ProjectUpdate'
db.create_table(u'main_projectupdate', ( db.create_table(u'main_projectupdate', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name="{'class': 'projectupdate', 'app_label': 'main'}(class)s_created", null=True, on_delete=models.SET_NULL, to=orm['auth.User'])),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=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)), ('active', self.gf('django.db.models.fields.BooleanField')(default=True)),
('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='project_updates', to=orm['main.Project'])), ('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)), ('cancel_flag', self.gf('django.db.models.fields.BooleanField')(default=False)),
('status', self.gf('django.db.models.fields.CharField')(default='new', max_length=20)), ('status', self.gf('django.db.models.fields.CharField')(default='new', max_length=20)),
@@ -46,6 +48,21 @@ class Migration(SchemaMigration):
self.gf('django.db.models.fields.BooleanField')(default=False), self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False) keep_default=False)
# Adding field 'Project.scm_delete_on_update'
db.add_column(u'main_project', 'scm_delete_on_update',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Adding field 'Project.scm_delete_on_next_update'
db.add_column(u'main_project', 'scm_delete_on_next_update',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Adding field 'Project.scm_update_on_launch'
db.add_column(u'main_project', 'scm_update_on_launch',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Adding field 'Project.scm_username' # Adding field 'Project.scm_username'
db.add_column(u'main_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), self.gf('django.db.models.fields.CharField')(default='', max_length=256, null=True, blank=True),
@@ -66,6 +83,16 @@ class Migration(SchemaMigration):
self.gf('django.db.models.fields.CharField')(default='', max_length=1024, null=True, blank=True), self.gf('django.db.models.fields.CharField')(default='', max_length=1024, null=True, blank=True),
keep_default=False) keep_default=False)
# Adding field 'Project.last_update'
db.add_column(u'main_project', 'last_update',
self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='project_as_last_update+', null=True, to=orm['main.ProjectUpdate']),
keep_default=False)
# Adding field 'Project.last_update_failed'
db.add_column(u'main_project', 'last_update_failed',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm): def backwards(self, orm):
# Deleting model 'ProjectUpdate' # Deleting model 'ProjectUpdate'
@@ -83,6 +110,15 @@ class Migration(SchemaMigration):
# Deleting field 'Project.scm_clean' # Deleting field 'Project.scm_clean'
db.delete_column(u'main_project', 'scm_clean') db.delete_column(u'main_project', 'scm_clean')
# Deleting field 'Project.scm_delete_on_update'
db.delete_column(u'main_project', 'scm_delete_on_update')
# Deleting field 'Project.scm_delete_on_next_update'
db.delete_column(u'main_project', 'scm_delete_on_next_update')
# Deleting field 'Project.scm_update_on_launch'
db.delete_column(u'main_project', 'scm_update_on_launch')
# Deleting field 'Project.scm_username' # Deleting field 'Project.scm_username'
db.delete_column(u'main_project', 'scm_username') db.delete_column(u'main_project', 'scm_username')
@@ -95,6 +131,12 @@ class Migration(SchemaMigration):
# Deleting field 'Project.scm_key_unlock' # Deleting field 'Project.scm_key_unlock'
db.delete_column(u'main_project', 'scm_key_unlock') db.delete_column(u'main_project', 'scm_key_unlock')
# Deleting field 'Project.last_update'
db.delete_column(u'main_project', 'last_update_id')
# Deleting field 'Project.last_update_failed'
db.delete_column(u'main_project', 'last_update_failed')
models = { models = {
u'auth.group': { u'auth.group': {
@@ -302,28 +344,35 @@ class Migration(SchemaMigration):
'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']"}), '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'}), 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'project_as_last_update+'", 'null': 'True', 'to': "orm['main.ProjectUpdate']"}),
'last_update_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), '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_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'null': 'True', 'blank': 'True'}),
'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'scm_delete_on_next_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'scm_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), 'scm_key_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_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_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_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'null': 'True', 'blank': 'True'}),
'scm_update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'scm_url': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '1024', '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'}) 'scm_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'null': 'True', 'blank': 'True'})
}, },
'main.projectupdate': { 'main.projectupdate': {
'Meta': {'object_name': 'ProjectUpdate'}, 'Meta': {'object_name': 'ProjectUpdate'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), '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': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'projectupdate\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'job_args': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': '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_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', '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']"}), '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_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),

View File

@@ -94,7 +94,10 @@ class PrimordialModel(models.Model):
tags = TaggableManager(blank=True) tags = TaggableManager(blank=True)
def __unicode__(self): def __unicode__(self):
return unicode("%s-%s"% (self.name, self.id)) if hasattr(self, 'name'):
return unicode("%s-%s"% (self.name, self.id))
else:
return u'%s-%s' % (self._meta.verbose_name, self.id)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# For compatibility with Django 1.4.x, attempt to handle any calls to # For compatibility with Django 1.4.x, attempt to handle any calls to
@@ -520,6 +523,7 @@ class Project(CommonModel):
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( scm_type = models.CharField(
max_length=8, max_length=8,
choices=SCM_TYPE_CHOICES, choices=SCM_TYPE_CHOICES,
@@ -546,6 +550,16 @@ class Project(CommonModel):
scm_clean = models.BooleanField( scm_clean = models.BooleanField(
default=False, default=False,
) )
scm_delete_on_update = models.BooleanField(
default=False,
)
scm_delete_on_next_update = models.BooleanField(
default=False,
editable=True,
)
scm_update_on_launch = models.BooleanField(
default=False,
)
scm_username = models.CharField( scm_username = models.CharField(
blank=True, blank=True,
null=True, null=True,
@@ -578,14 +592,47 @@ class Project(CommonModel):
help_text=_('Passphrase to unlock SSH private key if encrypted (or ' help_text=_('Passphrase to unlock SSH private key if encrypted (or '
'"ASK" to prompt the user).'), '"ASK" to prompt the user).'),
) )
last_update = models.ForeignKey(
'ProjectUpdate',
null=True,
default=None,
editable=False,
related_name='project_as_last_update+',
)
last_update_failed = models.BooleanField(
default=False,
editable=False,
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Check if scm_type or scm_url changes.
if self.pk:
project_before = Project.objects.get(pk=self.pk)
if project_before.scm_type != self.scm_type or project_before.scm_url != self.scm_url:
self.scm_delete_on_next_update = True
super(Project, self).save(*args, **kwargs) super(Project, self).save(*args, **kwargs)
if self.scm_type and not self.local_path.startswith('_'): if self.scm_type and not self.local_path.startswith('_'):
slug_name = slugify(unicode(self.name)).replace(u'-', u'_') slug_name = slugify(unicode(self.name)).replace(u'-', u'_')
self.local_path = u'_%d__%s' % (self.pk, slug_name) self.local_path = u'_%d__%s' % (self.pk, slug_name)
self.save(update_fields=['local_path']) self.save(update_fields=['local_path'])
@property
def needs_scm_password(self):
return not self.scm_key_data and self.ssh_password == 'ASK'
@property
def needs_scm_key_unlock(self):
return 'ENCRYPTED' in self.scm_key_data and \
(not self.scm_key_unlock or self.scm_key_unlock == 'ASK')
@property
def scm_passwords_needed(self):
needed = []
for field in ('scm_password', 'scm_key_unlock'):
if getattr(self, 'needs_%s' % field):
needed.append(field)
return needed
def update(self): def update(self):
if self.scm_type: if self.scm_type:
project_update = self.project_updates.create() project_update = self.project_updates.create()
@@ -593,11 +640,8 @@ class Project(CommonModel):
return project_update return project_update
@property @property
def last_update(self): def active_updates(self):
try: return self.project_updates.filter(active=True, status__in=('new', 'pending', 'running'))
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,))
@@ -641,20 +685,14 @@ class Project(CommonModel):
results.append(playbook) results.append(playbook)
return results return results
class ProjectUpdate(models.Model): class ProjectUpdate(PrimordialModel):
''' '''
Job for tracking internal project updates. Internal job for tracking project updates from SCM.
''' '''
class Meta: class Meta:
app_label = 'main' app_label = 'main'
created = models.DateTimeField(
auto_now_add=True,
)
modified = models.DateTimeField(
auto_now=True,
)
project = models.ForeignKey( project = models.ForeignKey(
'Project', 'Project',
related_name='project_updates', related_name='project_updates',
@@ -711,8 +749,26 @@ class ProjectUpdate(models.Model):
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Get status before save...
status_before = self.status or 'new'
if self.pk:
project_update_before = ProjectUpdate.objects.get(pk=self.pk)
if project_update_before.status != self.status:
status_before = project_update_before.status
self.failed = bool(self.status in ('failed', 'error', 'canceled')) self.failed = bool(self.status in ('failed', 'error', 'canceled'))
super(ProjectUpdate, self).save(*args, **kwargs) super(ProjectUpdate, self).save(*args, **kwargs)
# If status changed, and update has completed, update project.
if self.status != status_before:
if self.status in ('successful', 'failed', 'error', 'canceled'):
project = self.project
project.last_update = self
project.last_update_failed = self.failed
if not self.failed and project.scm_delete_on_next_update:
project.scm_delete_on_next_update = False
project.save()
def get_absolute_url(self):
return reverse('main:project_update_detail', args=(self.pk,))
@property @property
def celery_task(self): def celery_task(self):

View File

@@ -175,11 +175,17 @@ class OrganizationSerializer(BaseSerializer):
class ProjectSerializer(BaseSerializer): class ProjectSerializer(BaseSerializer):
playbooks = serializers.Field(source='playbooks', help_text='Array ') playbooks = serializers.Field(source='playbooks', help_text='Array of playbooks available within this project.')
scm_delete_on_next_update = serializers.Field(source='scm_delete_on_next_update')
class Meta: class Meta:
model = Project model = Project
fields = BASE_FIELDS + ('local_path',) fields = BASE_FIELDS + ('local_path', 'scm_type', 'scm_url',
'scm_branch', 'scm_clean',
'scm_delete_on_update', 'scm_delete_on_next_update',
'scm_update_on_launch',
'scm_username', 'scm_password', 'scm_key_data',
'scm_key_unlock', 'last_update_failed')
def get_related(self, obj): def get_related(self, obj):
res = super(ProjectSerializer, self).get_related(obj) res = super(ProjectSerializer, self).get_related(obj)
@@ -187,7 +193,12 @@ class ProjectSerializer(BaseSerializer):
organizations = reverse('main:project_organizations_list', args=(obj.pk,)), organizations = reverse('main:project_organizations_list', args=(obj.pk,)),
teams = reverse('main:project_teams_list', args=(obj.pk,)), teams = reverse('main:project_teams_list', args=(obj.pk,)),
playbooks = reverse('main:project_playbooks', args=(obj.pk,)), playbooks = reverse('main:project_playbooks', args=(obj.pk,)),
update = reverse('main:project_update_view', args=(obj.pk,)),
project_updates = reverse('main:project_updates_list', args=(obj.pk,)),
)) ))
if obj.last_update:
res['last_update'] = reverse('main:project_update_detail',
args=(obj.last_update.pk,))
return res return res
def validate_local_path(self, attrs, source): def validate_local_path(self, attrs, source):
@@ -209,6 +220,22 @@ class ProjectPlaybooksSerializer(ProjectSerializer):
ret = super(ProjectPlaybooksSerializer, self).to_native(obj) ret = super(ProjectPlaybooksSerializer, self).to_native(obj)
return ret.get('playbooks', []) return ret.get('playbooks', [])
class ProjectUpdateSerializer(BaseSerializer):
class Meta:
model = ProjectUpdate
fields = ('id', 'url', 'related', 'summary_fields', 'created',
'project', 'status', 'failed', 'result_stdout',
'result_traceback', 'job_args', 'job_cwd', 'job_env')
def get_related(self, obj):
res = super(ProjectUpdateSerializer, self).get_related(obj)
res.update(dict(
project = reverse('main:project_detail', args=(obj.project.pk,)),
cancel = reverse('main:project_update_cancel', args=(obj.pk,)),
))
return res
class BaseSerializerWithVariables(BaseSerializer): class BaseSerializerWithVariables(BaseSerializer):
def validate_variables(self, attrs, source): def validate_variables(self, attrs, source):

View File

@@ -10,6 +10,7 @@ import subprocess
import tempfile import tempfile
import time import time
import traceback import traceback
import urlparse
# Pexpect # Pexpect
import pexpect import pexpect
@@ -110,7 +111,7 @@ class BaseTask(Task):
r'Bad passphrase, try again for .*:': '', r'Bad passphrase, try again for .*:': '',
} }
def run_pexpect(self, pk, args, cwd, env, passwords): def run_pexpect(self, instance, args, cwd, env, passwords):
''' '''
Run the given command using pexpect to capture output and provide Run the given command using pexpect to capture output and provide
passwords when requested. passwords when requested.
@@ -134,13 +135,15 @@ class BaseTask(Task):
child.sendline(expect_passwords[result_id]) child.sendline(expect_passwords[result_id])
updates = {} updates = {}
if logfile_pos != logfile.tell(): if logfile_pos != logfile.tell():
logfile_pos = logfile.tell()
updates['result_stdout'] = logfile.getvalue() updates['result_stdout'] = logfile.getvalue()
last_stdout_update = time.time() last_stdout_update = time.time()
instance = self.update_model(pk, **updates) instance = self.update_model(instance.pk, **updates)
if instance.cancel_flag: if instance.cancel_flag:
child.close(True) child.close(True)
canceled = True canceled = True
#elif (time.time() - last_stdout_update) > 30: # FIXME: Configurable idle timeout? elif (time.time() - last_stdout_update) > 30: # FIXME: Configurable idle timeout?
print 'no updates...'
# print 'canceling...' # print 'canceling...'
# child.close(True) # child.close(True)
# canceled = True # canceled = True
@@ -153,10 +156,21 @@ class BaseTask(Task):
stdout = logfile.getvalue() stdout = logfile.getvalue()
return status, stdout return status, stdout
def pre_run_check(self, instance, **kwargs):
'''
Hook for checking job/task before running.
'''
if instance.status != 'pending':
return False
return True
def run(self, pk, **kwargs): def run(self, pk, **kwargs):
''' '''
Run the job/task using ansible-playbook and capture its output. Run the job/task using ansible-playbook and capture its output.
''' '''
instance = self.update_model(pk)
if not self.pre_run_check(instance, **kwargs):
return
instance = self.update_model(pk, status='running') instance = self.update_model(pk, status='running')
status, stdout, tb = 'error', '', '' status, stdout, tb = 'error', '', ''
try: try:
@@ -167,7 +181,7 @@ class BaseTask(Task):
env = self.build_env(instance, **kwargs) env = self.build_env(instance, **kwargs)
instance = self.update_model(pk, job_args=json.dumps(args), instance = self.update_model(pk, job_args=json.dumps(args),
job_cwd=cwd, job_env=env) job_cwd=cwd, job_env=env)
status, stdout = self.run_pexpect(pk, args, cwd, env, status, stdout = self.run_pexpect(instance, args, cwd, env,
kwargs['passwords']) kwargs['passwords'])
except Exception: except Exception:
tb = traceback.format_exc() tb = traceback.format_exc()
@@ -280,6 +294,15 @@ class RunJob(BaseTask):
}) })
return d return d
def pre_run_check(self, job, **kwargs):
'''
Hook for checking job before running.
'''
if not super(RunJob, self).pre_run_check(job, **kwargs):
return False
# FIXME: Check if job is waiting on any projects that are being updated.
return True
class RunProjectUpdate(BaseTask): class RunProjectUpdate(BaseTask):
name = 'run_project_update' name = 'run_project_update'
@@ -305,6 +328,19 @@ class RunProjectUpdate(BaseTask):
env = super(RunProjectUpdate, self).build_env(project_update, **kwargs) env = super(RunProjectUpdate, self).build_env(project_update, **kwargs)
return env return env
def update_url_auth(self, url, username=None, password=None):
parts = urlparse.urlsplit(url)
netloc_username = username or parts.username or ''
netloc_password = password or parts.password or ''
if netloc_username:
netloc = u':'.join(filter(None, [netloc_username, netloc_password]))
else:
netlock = u''
netloc = u'@'.join(filter(None, [netloc, parts.hostname]))
netloc = u':'.join(filter(None, [netloc, parts.port]))
return urlparse.urlunsplit([parts.scheme, netloc, parts.path,
parts.query, parts.fragment])
def build_args(self, project_update, **kwargs): def build_args(self, project_update, **kwargs):
''' '''
Build command line argument list for running ansible-playbook, Build command line argument list for running ansible-playbook,
@@ -312,16 +348,24 @@ class RunProjectUpdate(BaseTask):
''' '''
args = ['ansible-playbook', '-i', 'localhost,'] args = ['ansible-playbook', '-i', 'localhost,']
args.append('-%s' % ('v' * 3)) args.append('-%s' % ('v' * 3))
# FIXME
project = project_update.project project = project_update.project
scm_url = project.scm_url
if project.scm_username and project.scm_password:
scm_url = self.update_url_auth(scm_url, project.scm_username, project.scm_password)
elif project.scm_username:
scm_url = self.update_url_auth(scm_url, project.scm_username)
# FIXME: Need to hide password in saved job_args and result_stdout!
scm_branch = project.scm_branch or {'hg': 'tip'}.get(project.scm_type, 'HEAD')
scm_delete_on_update = project.scm_delete_on_update or project.scm_delete_on_next_update
extra_vars = { extra_vars = {
'project_path': project.get_project_path(check_if_exists=False), 'project_path': project.get_project_path(check_if_exists=False),
'scm_type': project.scm_type, 'scm_type': project.scm_type,
'scm_url': project.scm_url, 'scm_url': scm_url,
'scm_branch': project.scm_branch or 'HEAD', 'scm_branch': scm_branch,
'scm_clean': project.scm_clean, 'scm_clean': project.scm_clean,
'scm_username': project.scm_username, #'scm_username': project.scm_username,
'scm_password': project.scm_password, #'scm_password': project.scm_password,
'scm_delete_on_update': scm_delete_on_update,
} }
args.extend(['-e', json.dumps(extra_vars)]) args.extend(['-e', json.dumps(extra_vars)])
args.append('project_update.yml') args.append('project_update.yml')
@@ -348,3 +392,12 @@ class RunProjectUpdate(BaseTask):
r'Are you sure you want to continue connecting (yes/no)\?': 'yes', r'Are you sure you want to continue connecting (yes/no)\?': 'yes',
}) })
return d return d
def pre_run_check(self, project_update, **kwargs):
'''
Hook for checking project update before running.
'''
if not super(RunProjectUpdate, self).pre_run_check(project_update, **kwargs):
return False
# FIXME: Check if project update is blocked by any jobs that are being run.
return True

View File

@@ -1 +1,3 @@
{{ docstring }} {{ docstring }}
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}

View File

@@ -4,3 +4,5 @@ Make a GET request to this resource to retrieve the list of
{{ model_verbose_name_plural }}. {{ model_verbose_name_plural }}.
{% include "main/_list_common.md" %} {% include "main/_list_common.md" %}
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}

View File

@@ -8,3 +8,5 @@ fields to create a new {{ model_verbose_name }}:
{% with write_only=1 %} {% with write_only=1 %}
{% include "main/_result_fields_common.md" %} {% include "main/_result_fields_common.md" %}
{% endwith %} {% endwith %}
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}

View File

@@ -4,3 +4,6 @@ Make GET request to this resource to retrieve a single {{ model_verbose_name }}
record containing the following fields: record containing the following fields:
{% include "main/_result_fields_common.md" %} {% include "main/_result_fields_common.md" %}
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}

View File

@@ -16,3 +16,5 @@ For a PATCH request, include only the fields that are being modified.
# Delete {{ model_verbose_name|title }}: # Delete {{ model_verbose_name|title }}:
Make a DELETE request to this resource to delete this {{ model_verbose_name }}. Make a DELETE request to this resource to delete this {{ model_verbose_name }}.
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}

View File

@@ -5,3 +5,5 @@ Make a GET request to this resource to retrieve a list of
{{ parent_model_verbose_name }}. {{ parent_model_verbose_name }}.
{% include "main/_list_common.md" %} {% include "main/_list_common.md" %}
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}

View File

@@ -35,3 +35,5 @@ Make a POST request to this resource with `id` and `disassociate` fields to
remove the {{ model_verbose_name }} from this {{ parent_model_verbose_name }} remove the {{ model_verbose_name }} from this {{ parent_model_verbose_name }}
without deleting the {{ model_verbose_name }}. without deleting the {{ model_verbose_name }}.
{% endif %} {% endif %}
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}

View File

@@ -621,7 +621,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
def setUp(self): def setUp(self):
super(ProjectUpdatesTest, self).setUp() super(ProjectUpdatesTest, self).setUp()
self.setup_users() self.setup_users()
self.skipTest('blah') #self.skipTest('blah')
def create_project(self, **kwargs): def create_project(self, **kwargs):
project = Project.objects.create(**kwargs) project = Project.objects.create(**kwargs)
@@ -634,44 +634,103 @@ class ProjectUpdatesTest(BaseTransactionTest):
def check_project_update(self, project, should_fail=False): def check_project_update(self, project, should_fail=False):
print project.local_path #print project.local_path
pu = project.update() pu = project.update()
self.assertTrue(pu) self.assertTrue(pu)
pu = ProjectUpdate.objects.get(pk=pu.pk) pu = ProjectUpdate.objects.get(pk=pu.pk)
print pu.status #print pu.status
#print pu.result_traceback
if should_fail: if should_fail:
self.assertEqual(pu.status, 'failed', pu.result_stdout) self.assertEqual(pu.status, 'failed', pu.result_stdout)
else: else:
self.assertEqual(pu.status, 'successful', pu.result_stdout) self.assertEqual(pu.status, 'successful', pu.result_stdout)
project = Project.objects.get(pk=project.pk)
self.assertEqual(project.last_update, pu)
self.assertEqual(project.last_update_failed, pu.failed)
#print pu.result_traceback #print pu.result_traceback
#print pu.result_stdout #print pu.result_stdout
#print #print
return pu
def change_file_in_project(self, project): def change_file_in_project(self, project):
project_path = project.get_project_path() project_path = project.get_project_path()
self.assertTrue(project_path) self.assertTrue(project_path)
for root, dirs, files in os.walk(project_path): for root, dirs, files in os.walk(project_path):
for f in files: for f in files:
if f.startswith('.'): if f.startswith('.') or f == 'yadayada.txt':
continue
path_parts = os.path.relpath(root, project_path).split(os.sep)
if any([x.startswith('.') and x != '.' for x in path_parts]):
continue continue
path = os.path.join(root, f) path = os.path.join(root, f)
before = file(path, 'rb').read()
#print 'changed', path
file(path, 'wb').write('CHANGED FILE') file(path, 'wb').write('CHANGED FILE')
return after = file(path, 'rb').read()
return path, before, after
self.fail('no file found to change!') self.fail('no file found to change!')
def check_project_scm(self, project): def check_project_scm(self, project):
project_path = project.get_project_path(check_if_exists=False)
# Initial checkout. # Initial checkout.
self.assertFalse(os.path.exists(project_path))
self.check_project_update(project) self.check_project_update(project)
# Update to existing checkout. self.assertTrue(os.path.exists(project_path))
# Stick a new untracked file in the project.
untracked_path = os.path.join(project_path, 'yadayada.txt')
self.assertFalse(os.path.exists(untracked_path))
file(untracked_path, 'wb').write('yabba dabba doo')
self.assertTrue(os.path.exists(untracked_path))
# Update to existing checkout (should leave untracked file alone).
self.check_project_update(project) self.check_project_update(project)
# Change file then update (with scm_clean=False). self.assertTrue(os.path.exists(untracked_path))
# Change file then update (with scm_clean=False). Modified file should
# not be changed.
self.assertFalse(project.scm_clean) self.assertFalse(project.scm_clean)
self.change_file_in_project(project) modified_path, before, after = self.change_file_in_project(project)
self.check_project_update(project, should_fail=True) # Mercurial still returns successful if a modified file is present.
# Set scm_clean=True then try to update again. should_fail = bool(project.scm_type != 'hg')
self.check_project_update(project, should_fail=should_fail)
content = file(modified_path, 'rb').read()
self.assertEqual(content, after)
self.assertTrue(os.path.exists(untracked_path))
# Set scm_clean=True then try to update again. Modified file should
# have been replaced with the original. Untracked file should still be
# present.
project.scm_clean = True project.scm_clean = True
project.save() project.save()
self.check_project_update(project) self.check_project_update(project)
content = file(modified_path, 'rb').read()
self.assertEqual(content, before)
self.assertTrue(os.path.exists(untracked_path))
# If scm_type or scm_url changes, scm_delete_on_next_update should be
# set, causing project directory (including untracked file) to be
# completely blown away, but only for the next update..
self.assertFalse(project.scm_delete_on_update)
self.assertFalse(project.scm_delete_on_next_update)
scm_type = project.scm_type
project.scm_type = ''
project.save()
self.assertTrue(project.scm_delete_on_next_update)
project.scm_type = scm_type
project.save()
self.check_project_update(project)
self.assertFalse(os.path.exists(untracked_path))
# Check that the flag is cleared after the update, and that an
# untracked file isn't blown away.
project = Project.objects.get(pk=project.pk)
self.assertFalse(project.scm_delete_on_next_update)
file(untracked_path, 'wb').write('yabba dabba doo')
self.assertTrue(os.path.exists(untracked_path))
self.check_project_update(project)
self.assertTrue(os.path.exists(untracked_path))
# Set scm_delete_on_update=True then update again. Project directory
# (including untracked file) should be completely blown away.
self.assertFalse(project.scm_delete_on_update)
project.scm_delete_on_update = True
project.save()
self.check_project_update(project)
self.assertFalse(os.path.exists(untracked_path))
def test_public_git_project_over_https(self): def test_public_git_project_over_https(self):
scm_url = getattr(settings, 'TEST_GIT_PUBLIC_HTTPS', scm_url = getattr(settings, 'TEST_GIT_PUBLIC_HTTPS',

View File

@@ -36,6 +36,13 @@ project_urls = patterns('awx.main.views',
url(r'^(?P<pk>[0-9]+)/playbooks/$', 'project_playbooks'), url(r'^(?P<pk>[0-9]+)/playbooks/$', 'project_playbooks'),
url(r'^(?P<pk>[0-9]+)/organizations/$', 'project_organizations_list'), url(r'^(?P<pk>[0-9]+)/organizations/$', 'project_organizations_list'),
url(r'^(?P<pk>[0-9]+)/teams/$', 'project_teams_list'), url(r'^(?P<pk>[0-9]+)/teams/$', 'project_teams_list'),
url(r'^(?P<pk>[0-9]+)/update/$', 'project_update_view'),
url(r'^(?P<pk>[0-9]+)/updates/$', 'project_updates_list'),
)
project_update_urls = patterns('awx.main.views',
url(r'^(?P<pk>[0-9]+)/$', 'project_update_detail'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'project_update_cancel'),
) )
team_urls = patterns('awx.main.views', team_urls = patterns('awx.main.views',
@@ -116,23 +123,24 @@ job_event_urls = patterns('awx.main.views',
) )
v1_urls = patterns('awx.main.views', v1_urls = patterns('awx.main.views',
url(r'^$', 'api_v1_root_view'), url(r'^$', 'api_v1_root_view'),
url(r'^config/$', 'api_v1_config_view'), url(r'^config/$', 'api_v1_config_view'),
url(r'^authtoken/$', 'auth_token_view'), url(r'^authtoken/$', 'auth_token_view'),
url(r'^me/$', 'user_me_list'), url(r'^me/$', 'user_me_list'),
url(r'^organizations/', include(organization_urls)), url(r'^organizations/', include(organization_urls)),
url(r'^users/', include(user_urls)), url(r'^users/', include(user_urls)),
url(r'^projects/', include(project_urls)), url(r'^projects/', include(project_urls)),
url(r'^teams/', include(team_urls)), url(r'^project_updates/', include(project_update_urls)),
url(r'^inventories/', include(inventory_urls)), url(r'^teams/', include(team_urls)),
url(r'^hosts/', include(host_urls)), url(r'^inventories/', include(inventory_urls)),
url(r'^groups/', include(group_urls)), url(r'^hosts/', include(host_urls)),
url(r'^credentials/', include(credential_urls)), url(r'^groups/', include(group_urls)),
url(r'^permissions/', include(permission_urls)), url(r'^credentials/', include(credential_urls)),
url(r'^job_templates/', include(job_template_urls)), url(r'^permissions/', include(permission_urls)),
url(r'^jobs/', include(job_urls)), url(r'^job_templates/', include(job_template_urls)),
url(r'^job_host_summaries/', include(job_host_summary_urls)), url(r'^jobs/', include(job_urls)),
url(r'^job_events/', include(job_event_urls)), url(r'^job_host_summaries/', include(job_host_summary_urls)),
url(r'^job_events/', include(job_event_urls)),
) )
urlpatterns = patterns('awx.main.views', urlpatterns = patterns('awx.main.views',

View File

@@ -251,6 +251,67 @@ class ProjectTeamsList(SubListCreateAPIView):
parent_model = Project parent_model = Project
relationship = 'teams' relationship = 'teams'
class ProjectUpdatesList(SubListAPIView):
model = ProjectUpdate
serializer_class = ProjectUpdateSerializer
parent_model = Project
relationship = 'project_updates'
new_in_13 = True
class ProjectUpdateView(GenericAPIView):
model = Project
new_in_13 = True
def get(self, request, *args, **kwargs):
obj = self.get_object()
data = dict(
can_update=bool(obj.scm_type),
)
#if obj.scm_type:
# data['passwords_needed_to_update'] = obj.get_passwords_needed_to_start()
return Response(data)
def post(self, request, *args, **kwargs):
obj = self.get_object()
if bool(obj.scm_type):
project_update = obj.update()
if not project_update:
data = dict(msg='Unable to update project!')
return Response(data, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(status=status.HTTP_202_ACCEPTED)
else:
return self.http_method_not_allowed(request, *args, **kwargs)
class ProjectUpdateDetail(RetrieveAPIView):
model = ProjectUpdate
serializer_class = ProjectUpdateSerializer
new_in_13 = True
class ProjectUpdateCancel(GenericAPIView):
model = ProjectUpdate
is_job_cancel = True
new_in_13 = True
def get(self, request, *args, **kwargs):
obj = self.get_object()
data = dict(
can_cancel=obj.can_cancel,
)
return Response(data)
def post(self, request, *args, **kwargs):
obj = self.get_object()
if obj.can_cancel:
result = obj.cancel()
return Response(status=status.HTTP_202_ACCEPTED)
else:
return self.http_method_not_allowed(request, *args, **kwargs)
class UserList(ListCreateAPIView): class UserList(ListCreateAPIView):
model = User model = User

View File

@@ -6,32 +6,31 @@
# scm_url: https://server/repo # scm_url: https://server/repo
# scm_branch: HEAD # scm_branch: HEAD
# scm_clean: true/false # scm_clean: true/false
# scm_delete_on_update: true/false
- hosts: all - hosts: all
connection: local connection: local
gather_facts: false gather_facts: false
tasks: tasks:
# Git Tasks - name: delete project directory before update
file: path={{project_path}} state=absent
when: scm_delete_on_update
- name: update project using git - name: update project using git
git: dest={{project_path}} repo={{scm_url}} version={{scm_branch}} force={{scm_clean}} git: dest={{project_path}} repo={{scm_url}} version={{scm_branch}} force={{scm_clean}}
when: scm_type == 'git' when: scm_type == 'git'
async: 0 async: 0
poll: 5 poll: 5
tags: git
# Mercurial Tasks
- name: update project using hg - name: update project using hg
hg: dest={{project_path}} repo={{scm_url}} version={{scm_branch}} force={{scm_clean}} hg: dest={{project_path}} repo={{scm_url}} revision={{scm_branch}} force={{scm_clean}}
when: scm_type == 'hg' when: scm_type == 'hg'
async: 0 async: 0
poll: 5 poll: 5
tags: hg
# Subversion Tasks
- name: update project using svn - name: update project using svn
subversion: dest={{project_path}} repo={{scm_url}} revision={{scm_branch}} force={{scm_clean}} subversion: dest={{project_path}} repo={{scm_url}} revision={{scm_branch}} force={{scm_clean}}
when: scm_type == 'svn' when: scm_type == 'svn'
async: 0 async: 0
poll: 5 poll: 5
tags: svn