mirror of
https://github.com/ansible/awx.git
synced 2026-03-13 23:17:32 -02:30
More work in progress on AC-132.
This commit is contained in:
@@ -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 *
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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'}),
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
{{ docstring }}
|
{{ docstring }}
|
||||||
|
|
||||||
|
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user