1
0
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:
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 *.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 *

View File

@ -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)

View File

@ -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):

View File

@ -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'}),

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -1 +1,3 @@
{{ 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 }}.
{% 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 %}
{% include "main/_result_fields_common.md" %}
{% 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:
{% 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 }}:
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 }}.
{% 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 }}
without deleting the {{ model_verbose_name }}.
{% endif %}
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}

View File

@ -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',

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]+)/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',

View File

@ -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

View File

@ -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