mirror of
https://github.com/ansible/awx.git
synced 2024-10-26 16:25:06 +03:00
More work in progress on AC-132.
This commit is contained in:
parent
e594296c9b
commit
ee3ba2c0e1
@ -6,6 +6,7 @@ recursive-include awx/ui *.html
|
||||
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/lib *
|
||||
recursive-include awx/playbooks *.yml
|
||||
recursive-include awx/lib/site-packages *
|
||||
recursive-include config *
|
||||
recursive-include config/deb *
|
||||
|
@ -601,6 +601,33 @@ class ProjectAccess(BaseAccess):
|
||||
def can_delete(self, obj):
|
||||
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):
|
||||
'''
|
||||
I can see a permission when:
|
||||
@ -944,6 +971,7 @@ register_access(Group, GroupAccess)
|
||||
register_access(Credential, CredentialAccess)
|
||||
register_access(Team, TeamAccess)
|
||||
register_access(Project, ProjectAccess)
|
||||
register_access(ProjectUpdate, ProjectUpdateAccess)
|
||||
register_access(Permission, PermissionAccess)
|
||||
register_access(JobTemplate, JobTemplateAccess)
|
||||
register_access(Job, JobAccess)
|
||||
|
@ -34,6 +34,7 @@ class APIView(views.APIView):
|
||||
def get_description_context(self):
|
||||
return {
|
||||
'docstring': type(self).__doc__ or '',
|
||||
'new_in_13': getattr(self, 'new_in_13', False),
|
||||
}
|
||||
|
||||
def get_description(self, html=False):
|
||||
|
@ -11,8 +11,10 @@ class Migration(SchemaMigration):
|
||||
# Adding model 'ProjectUpdate'
|
||||
db.create_table(u'main_projectupdate', (
|
||||
(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)),
|
||||
('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'])),
|
||||
('cancel_flag', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||
('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),
|
||||
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'
|
||||
db.add_column(u'main_project', 'scm_username',
|
||||
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),
|
||||
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):
|
||||
# Deleting model 'ProjectUpdate'
|
||||
@ -83,6 +110,15 @@ class Migration(SchemaMigration):
|
||||
# Deleting field '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'
|
||||
db.delete_column(u'main_project', 'scm_username')
|
||||
|
||||
@ -95,6 +131,12 @@ class Migration(SchemaMigration):
|
||||
# Deleting field '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 = {
|
||||
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']"}),
|
||||
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': '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'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
|
||||
'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'null': 'True', 'blank': 'True'}),
|
||||
'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'scm_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_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'null': 'True', 'blank': 'True'}),
|
||||
'scm_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'null': 'True', 'blank': 'True'}),
|
||||
'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'null': 'True', 'blank': 'True'}),
|
||||
'scm_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_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'main.projectupdate': {
|
||||
'Meta': {'object_name': 'ProjectUpdate'},
|
||||
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'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'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'job_args': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||
'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||
'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
|
||||
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
|
||||
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_updates'", 'to': u"orm['main.Project']"}),
|
||||
'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||
'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||
|
@ -94,7 +94,10 @@ class PrimordialModel(models.Model):
|
||||
tags = TaggableManager(blank=True)
|
||||
|
||||
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):
|
||||
# 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 '
|
||||
'playbooks and related files for this project.')
|
||||
)
|
||||
|
||||
scm_type = models.CharField(
|
||||
max_length=8,
|
||||
choices=SCM_TYPE_CHOICES,
|
||||
@ -546,6 +550,16 @@ class Project(CommonModel):
|
||||
scm_clean = models.BooleanField(
|
||||
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(
|
||||
blank=True,
|
||||
null=True,
|
||||
@ -578,14 +592,47 @@ class Project(CommonModel):
|
||||
help_text=_('Passphrase to unlock SSH private key if encrypted (or '
|
||||
'"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):
|
||||
# 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)
|
||||
if self.scm_type and not self.local_path.startswith('_'):
|
||||
slug_name = slugify(unicode(self.name)).replace(u'-', u'_')
|
||||
self.local_path = u'_%d__%s' % (self.pk, slug_name)
|
||||
self.save(update_fields=['local_path'])
|
||||
|
||||
@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):
|
||||
if self.scm_type:
|
||||
project_update = self.project_updates.create()
|
||||
@ -593,11 +640,8 @@ class Project(CommonModel):
|
||||
return project_update
|
||||
|
||||
@property
|
||||
def last_update(self):
|
||||
try:
|
||||
return self.project_updates.order_by('-modified')[0]
|
||||
except IndexError:
|
||||
pass
|
||||
def active_updates(self):
|
||||
return self.project_updates.filter(active=True, status__in=('new', 'pending', 'running'))
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('main:project_detail', args=(self.pk,))
|
||||
@ -641,20 +685,14 @@ class Project(CommonModel):
|
||||
results.append(playbook)
|
||||
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:
|
||||
app_label = 'main'
|
||||
|
||||
created = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
)
|
||||
modified = models.DateTimeField(
|
||||
auto_now=True,
|
||||
)
|
||||
project = models.ForeignKey(
|
||||
'Project',
|
||||
related_name='project_updates',
|
||||
@ -711,8 +749,26 @@ class ProjectUpdate(models.Model):
|
||||
)
|
||||
|
||||
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'))
|
||||
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
|
||||
def celery_task(self):
|
||||
|
@ -175,11 +175,17 @@ class OrganizationSerializer(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:
|
||||
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):
|
||||
res = super(ProjectSerializer, self).get_related(obj)
|
||||
@ -187,7 +193,12 @@ class ProjectSerializer(BaseSerializer):
|
||||
organizations = reverse('main:project_organizations_list', args=(obj.pk,)),
|
||||
teams = reverse('main:project_teams_list', 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
|
||||
|
||||
def validate_local_path(self, attrs, source):
|
||||
@ -209,6 +220,22 @@ class ProjectPlaybooksSerializer(ProjectSerializer):
|
||||
ret = super(ProjectPlaybooksSerializer, self).to_native(obj)
|
||||
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):
|
||||
|
||||
def validate_variables(self, attrs, source):
|
||||
|
@ -10,6 +10,7 @@ import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
import urlparse
|
||||
|
||||
# Pexpect
|
||||
import pexpect
|
||||
@ -110,7 +111,7 @@ class BaseTask(Task):
|
||||
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
|
||||
passwords when requested.
|
||||
@ -134,13 +135,15 @@ class BaseTask(Task):
|
||||
child.sendline(expect_passwords[result_id])
|
||||
updates = {}
|
||||
if logfile_pos != logfile.tell():
|
||||
logfile_pos = logfile.tell()
|
||||
updates['result_stdout'] = logfile.getvalue()
|
||||
last_stdout_update = time.time()
|
||||
instance = self.update_model(pk, **updates)
|
||||
instance = self.update_model(instance.pk, **updates)
|
||||
if instance.cancel_flag:
|
||||
child.close(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...'
|
||||
# child.close(True)
|
||||
# canceled = True
|
||||
@ -153,10 +156,21 @@ class BaseTask(Task):
|
||||
stdout = logfile.getvalue()
|
||||
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):
|
||||
'''
|
||||
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')
|
||||
status, stdout, tb = 'error', '', ''
|
||||
try:
|
||||
@ -167,7 +181,7 @@ class BaseTask(Task):
|
||||
env = self.build_env(instance, **kwargs)
|
||||
instance = self.update_model(pk, job_args=json.dumps(args),
|
||||
job_cwd=cwd, job_env=env)
|
||||
status, stdout = self.run_pexpect(pk, args, cwd, env,
|
||||
status, stdout = self.run_pexpect(instance, args, cwd, env,
|
||||
kwargs['passwords'])
|
||||
except Exception:
|
||||
tb = traceback.format_exc()
|
||||
@ -280,6 +294,15 @@ class RunJob(BaseTask):
|
||||
})
|
||||
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):
|
||||
|
||||
name = 'run_project_update'
|
||||
@ -305,6 +328,19 @@ class RunProjectUpdate(BaseTask):
|
||||
env = super(RunProjectUpdate, self).build_env(project_update, **kwargs)
|
||||
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):
|
||||
'''
|
||||
Build command line argument list for running ansible-playbook,
|
||||
@ -312,16 +348,24 @@ class RunProjectUpdate(BaseTask):
|
||||
'''
|
||||
args = ['ansible-playbook', '-i', 'localhost,']
|
||||
args.append('-%s' % ('v' * 3))
|
||||
# FIXME
|
||||
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 = {
|
||||
'project_path': project.get_project_path(check_if_exists=False),
|
||||
'scm_type': project.scm_type,
|
||||
'scm_url': project.scm_url,
|
||||
'scm_branch': project.scm_branch or 'HEAD',
|
||||
'scm_url': scm_url,
|
||||
'scm_branch': scm_branch,
|
||||
'scm_clean': project.scm_clean,
|
||||
'scm_username': project.scm_username,
|
||||
'scm_password': project.scm_password,
|
||||
#'scm_username': project.scm_username,
|
||||
#'scm_password': project.scm_password,
|
||||
'scm_delete_on_update': scm_delete_on_update,
|
||||
}
|
||||
args.extend(['-e', json.dumps(extra_vars)])
|
||||
args.append('project_update.yml')
|
||||
@ -348,3 +392,12 @@ class RunProjectUpdate(BaseTask):
|
||||
r'Are you sure you want to continue connecting (yes/no)\?': 'yes',
|
||||
})
|
||||
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 }}
|
||||
|
||||
{% 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 }}.
|
||||
|
||||
{% 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 %}
|
||||
{% include "main/_result_fields_common.md" %}
|
||||
{% 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:
|
||||
|
||||
{% 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 }}:
|
||||
|
||||
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 }}.
|
||||
|
||||
{% 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 }}
|
||||
without deleting the {{ model_verbose_name }}.
|
||||
{% endif %}
|
||||
|
||||
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}
|
||||
|
@ -621,7 +621,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
|
||||
def setUp(self):
|
||||
super(ProjectUpdatesTest, self).setUp()
|
||||
self.setup_users()
|
||||
self.skipTest('blah')
|
||||
#self.skipTest('blah')
|
||||
|
||||
def create_project(self, **kwargs):
|
||||
project = Project.objects.create(**kwargs)
|
||||
@ -634,44 +634,103 @@ class ProjectUpdatesTest(BaseTransactionTest):
|
||||
|
||||
|
||||
def check_project_update(self, project, should_fail=False):
|
||||
print project.local_path
|
||||
#print project.local_path
|
||||
pu = project.update()
|
||||
self.assertTrue(pu)
|
||||
pu = ProjectUpdate.objects.get(pk=pu.pk)
|
||||
print pu.status
|
||||
#print pu.status
|
||||
#print pu.result_traceback
|
||||
if should_fail:
|
||||
self.assertEqual(pu.status, 'failed', pu.result_stdout)
|
||||
else:
|
||||
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_stdout
|
||||
#print
|
||||
return pu
|
||||
|
||||
def change_file_in_project(self, project):
|
||||
project_path = project.get_project_path()
|
||||
self.assertTrue(project_path)
|
||||
for root, dirs, files in os.walk(project_path):
|
||||
for f in files:
|
||||
if f.startswith('.'):
|
||||
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
|
||||
path = os.path.join(root, f)
|
||||
before = file(path, 'rb').read()
|
||||
#print 'changed', path
|
||||
file(path, 'wb').write('CHANGED FILE')
|
||||
return
|
||||
after = file(path, 'rb').read()
|
||||
return path, before, after
|
||||
self.fail('no file found to change!')
|
||||
|
||||
def check_project_scm(self, project):
|
||||
project_path = project.get_project_path(check_if_exists=False)
|
||||
# Initial checkout.
|
||||
self.assertFalse(os.path.exists(project_path))
|
||||
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)
|
||||
# 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.change_file_in_project(project)
|
||||
self.check_project_update(project, should_fail=True)
|
||||
# Set scm_clean=True then try to update again.
|
||||
modified_path, before, after = self.change_file_in_project(project)
|
||||
# Mercurial still returns successful if a modified file is present.
|
||||
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.save()
|
||||
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):
|
||||
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]+)/organizations/$', 'project_organizations_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',
|
||||
@ -116,23 +123,24 @@ job_event_urls = patterns('awx.main.views',
|
||||
)
|
||||
|
||||
v1_urls = patterns('awx.main.views',
|
||||
url(r'^$', 'api_v1_root_view'),
|
||||
url(r'^config/$', 'api_v1_config_view'),
|
||||
url(r'^authtoken/$', 'auth_token_view'),
|
||||
url(r'^me/$', 'user_me_list'),
|
||||
url(r'^organizations/', include(organization_urls)),
|
||||
url(r'^users/', include(user_urls)),
|
||||
url(r'^projects/', include(project_urls)),
|
||||
url(r'^teams/', include(team_urls)),
|
||||
url(r'^inventories/', include(inventory_urls)),
|
||||
url(r'^hosts/', include(host_urls)),
|
||||
url(r'^groups/', include(group_urls)),
|
||||
url(r'^credentials/', include(credential_urls)),
|
||||
url(r'^permissions/', include(permission_urls)),
|
||||
url(r'^job_templates/', include(job_template_urls)),
|
||||
url(r'^jobs/', include(job_urls)),
|
||||
url(r'^job_host_summaries/', include(job_host_summary_urls)),
|
||||
url(r'^job_events/', include(job_event_urls)),
|
||||
url(r'^$', 'api_v1_root_view'),
|
||||
url(r'^config/$', 'api_v1_config_view'),
|
||||
url(r'^authtoken/$', 'auth_token_view'),
|
||||
url(r'^me/$', 'user_me_list'),
|
||||
url(r'^organizations/', include(organization_urls)),
|
||||
url(r'^users/', include(user_urls)),
|
||||
url(r'^projects/', include(project_urls)),
|
||||
url(r'^project_updates/', include(project_update_urls)),
|
||||
url(r'^teams/', include(team_urls)),
|
||||
url(r'^inventories/', include(inventory_urls)),
|
||||
url(r'^hosts/', include(host_urls)),
|
||||
url(r'^groups/', include(group_urls)),
|
||||
url(r'^credentials/', include(credential_urls)),
|
||||
url(r'^permissions/', include(permission_urls)),
|
||||
url(r'^job_templates/', include(job_template_urls)),
|
||||
url(r'^jobs/', include(job_urls)),
|
||||
url(r'^job_host_summaries/', include(job_host_summary_urls)),
|
||||
url(r'^job_events/', include(job_event_urls)),
|
||||
)
|
||||
|
||||
urlpatterns = patterns('awx.main.views',
|
||||
|
@ -251,6 +251,67 @@ class ProjectTeamsList(SubListCreateAPIView):
|
||||
parent_model = Project
|
||||
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):
|
||||
|
||||
model = User
|
||||
|
@ -6,32 +6,31 @@
|
||||
# scm_url: https://server/repo
|
||||
# scm_branch: HEAD
|
||||
# scm_clean: true/false
|
||||
# scm_delete_on_update: true/false
|
||||
|
||||
- hosts: all
|
||||
connection: local
|
||||
gather_facts: false
|
||||
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
|
||||
git: dest={{project_path}} repo={{scm_url}} version={{scm_branch}} force={{scm_clean}}
|
||||
when: scm_type == 'git'
|
||||
async: 0
|
||||
poll: 5
|
||||
tags: git
|
||||
|
||||
# Mercurial Tasks
|
||||
- name: update project using hg
|
||||
hg: dest={{project_path}} repo={{scm_url}} version={{scm_branch}} force={{scm_clean}}
|
||||
hg: dest={{project_path}} repo={{scm_url}} revision={{scm_branch}} force={{scm_clean}}
|
||||
when: scm_type == 'hg'
|
||||
async: 0
|
||||
poll: 5
|
||||
tags: hg
|
||||
|
||||
# Subversion Tasks
|
||||
- name: update project using svn
|
||||
subversion: dest={{project_path}} repo={{scm_url}} revision={{scm_branch}} force={{scm_clean}}
|
||||
when: scm_type == 'svn'
|
||||
async: 0
|
||||
poll: 5
|
||||
tags: svn
|
||||
|
Loading…
Reference in New Issue
Block a user