diff --git a/lib/main/admin.py b/lib/main/admin.py index d02f9135e8..357f56f5d4 100644 --- a/lib/main/admin.py +++ b/lib/main/admin.py @@ -217,19 +217,20 @@ class ProjectAdmin(BaseModelAdmin): list_display = ('name', 'description', 'active') fieldsets = ( (None, {'fields': (('name', 'active'), 'description', 'local_path', - 'get_available_playbooks_display')}), + 'get_playbooks_display')}), (_('Tags'), {'fields': ('tags',)}), (_('Audit Trail'), {'fields': ('created', 'created_by', 'audit_trail',)}), ) readonly_fields = ('created', 'created_by', 'audit_trail', - 'get_available_playbooks_display') + 'get_playbooks_display') filter_horizontal = ('tags',) + form = ProjectAdminForm - def get_available_playbooks_display(self, obj): + def get_playbooks_display(self, obj): return '
'.join([format_html('{0}', x) for x in - obj.available_playbooks]) - get_available_playbooks_display.short_description = _('Available playbooks') - get_available_playbooks_display.allow_tags = True + obj.playbooks]) + get_playbooks_display.short_description = _('Playbooks') + get_playbooks_display.allow_tags = True class PermissionAdmin(BaseModelAdmin): diff --git a/lib/main/forms.py b/lib/main/forms.py index c90c14c462..a94ef1b0ff 100644 --- a/lib/main/forms.py +++ b/lib/main/forms.py @@ -68,6 +68,18 @@ class GroupForm(forms.ModelForm): variable_data = JSONFormField(required=False, widget=forms.Textarea(attrs={'class': 'vLargeTextField'})) +class ProjectAdminForm(forms.ModelForm): + '''Custom admin form for Projects.''' + + local_path = forms.ChoiceField(choices=[]) + + class Meta: + model = Project + + def __init__(self, *args, **kwargs): + super(ProjectAdminForm, self).__init__(*args, **kwargs) + self.fields['local_path'].choices = [(x, x) for x in Project.get_local_path_choices()] + class JobTemplateAdminForm(forms.ModelForm): '''Custom admin form for creating/editing JobTemplates.''' @@ -80,7 +92,7 @@ class JobTemplateAdminForm(forms.ModelForm): super(JobTemplateAdminForm, self).__init__(*args, **kwargs) playbook_choices = [] for project in Project.objects.all(): - for playbook in project.available_playbooks: + for playbook in project.playbooks: playbook_choices.append((playbook, PlaybookOption(project, playbook))) self.fields['playbook'].choices = [EMPTY_CHOICE] + playbook_choices diff --git a/lib/main/migrations/0017_changes.py b/lib/main/migrations/0017_changes.py new file mode 100644 index 0000000000..7bbcfe2a96 --- /dev/null +++ b/lib/main/migrations/0017_changes.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Removing unique constraint on 'Project', fields ['local_path'] + db.delete_unique(u'main_project', ['local_path']) + + + # Changing field 'Project.local_path' + db.alter_column(u'main_project', 'local_path', self.gf('django.db.models.fields.CharField')(max_length=1024)) + + # Change absolute paths to only the directory name instead. + if not db.dry_run: + import os + for project in orm.Project.objects.all(): + project.local_path = os.path.basename(project.local_path) + project.save() + + def backwards(self, orm): + + # Change directory names back to aboslute paths. + if not db.dry_run: + import os + from django.conf import settings + for project in orm.Project.objects.all(): + project.local_path = os.path.join(settings.PROJECTS_ROOT, os.path.basename(project.local_path)) + project.save() + + # Changing field 'Project.local_path' + db.alter_column(u'main_project', 'local_path', self.gf('django.db.models.fields.FilePathField')(max_length=1024, path='/Users/chris/Sandbox/ansible-commander/lib/projects', unique=True)) + # Adding unique constraint on 'Project', fields ['local_path'] + db.create_unique(u'main_project', ['local_path']) + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.audittrail': { + 'Meta': {'object_name': 'AuditTrail'}, + 'comment': ('django.db.models.fields.TextField', [], {}), + 'delta': ('django.db.models.fields.TextField', [], {}), + 'detail': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'resource_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Tag']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}) + }, + 'main.credential': { + 'Meta': {'object_name': 'Credential'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'credential_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'ssh_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'ssh_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'ssh_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'credential_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Team']", 'blank': 'True', 'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['auth.User']", 'blank': 'True', 'null': 'True'}) + }, + 'main.group': { + 'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Group'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'group_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'children'", 'blank': 'True', 'to': "orm['main.Group']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'group_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'variable_data': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'group'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.VariableData']", 'blank': 'True', 'null': 'True'}) + }, + 'main.host': { + 'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Host'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'host_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Job']", 'blank': 'True', 'null': 'True'}), + 'last_job_host_summary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job_summary+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['main.JobHostSummary']", 'blank': 'True', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'host_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'variable_data': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'host'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.VariableData']", 'blank': 'True', 'null': 'True'}) + }, + 'main.inventory': { + 'Meta': {'unique_together': "(('name', 'organization'),)", 'object_name': 'Inventory'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'inventory_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventories'", 'to': "orm['main.Organization']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'inventory_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}) + }, + 'main.job': { + 'Meta': {'object_name': 'Job'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'job_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'job\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Credential']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobs'", 'blank': 'True', 'through': u"orm['main.JobHostSummary']", 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobTemplate']", 'blank': 'True', 'null': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'playbook': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'job_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.jobevent': { + 'Meta': {'ordering': "('pk',)", 'object_name': 'JobEvent'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'event_data': ('jsonfield.fields.JSONField', [], {'default': "''", 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Host']", 'blank': 'True', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'to': "orm['main.Job']"}) + }, + u'main.jobhostsummary': { + 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('job', 'host')]", 'object_name': 'JobHostSummary'}, + 'changed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dark': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Job']"}), + 'ok': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'processed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'skipped': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'main.jobtemplate': { + 'Meta': {'object_name': 'JobTemplate'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobtemplate_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'jobtemplate\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobtemplate_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.organization': { + 'Meta': {'object_name': 'Organization'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organization_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['main.Project']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organization_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.permission': { + 'Meta': {'object_name': 'Permission'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'permission_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'permission_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}) + }, + u'main.project': { + 'Meta': {'object_name': 'Project'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'project_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'project\', \'app_label\': u\'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'project_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}) + }, + 'main.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}) + }, + 'main.team': { + 'Meta': {'object_name': 'Team'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'team_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'teams'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['main.Project']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'team_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.variabledata': { + 'Meta': {'object_name': 'VariableData'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'variabledata_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'variabledata\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'data': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'variabledata_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}) + } + } + + complete_apps = ['main'] \ No newline at end of file diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py index 948e92864a..1af2ed7ab4 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -370,13 +370,25 @@ class Project(CommonModel): # this is not part of the project, but managed with perms # inventories = models.ManyToManyField('Inventory', blank=True, related_name='projects') - local_path = models.FilePathField( - path=settings.PROJECTS_ROOT, - recursive=False, - allow_files=False, - allow_folders=True, + # Project files must be available on the server in folders directly + # beneath the path specified by settings.PROJECTS_ROOT. There is no way + # via the API to upload/update a project or its playbooks; this must be + # done by other means for now. + + @classmethod + def get_local_path_choices(cls): + if os.path.exists(settings.PROJECTS_ROOT): + return [x for x in os.listdir(settings.PROJECTS_ROOT) + if os.path.isdir(os.path.join(settings.PROJECTS_ROOT, x)) + and not x.startswith('.')] + else: + return [] + + local_path = models.CharField( max_length=1024, - unique=True, + # Not unique for now, otherwise "deletes" won't allow reusing the + # same path for another active project. + #unique=True, help_text=_('Local path (relative to PROJECTS_ROOT) containing ' 'playbooks and related files for this project.') ) @@ -386,11 +398,19 @@ class Project(CommonModel): def get_absolute_url(self): return reverse('main:projects_detail', args=(self.pk,)) + def get_project_path(self): + local_path = os.path.basename(self.local_path) + if local_path and not local_path.startswith('.'): + proj_path = os.path.join(settings.PROJECTS_ROOT, local_path) + if os.path.exists(proj_path): + return proj_path + @property - def available_playbooks(self): - playbooks = [] - if self.local_path and os.path.exists(self.local_path): - for dirpath, dirnames, filenames in os.walk(self.local_path): + def playbooks(self): + results = [] + project_path = self.get_project_path() + if project_path: + for dirpath, dirnames, filenames in os.walk(project_path): for filename in filenames: if os.path.splitext(filename)[-1] != '.yml': continue @@ -407,15 +427,15 @@ class Project(CommonModel): continue except (TypeError, IndexError, KeyError): continue - playbook = os.path.relpath(playbook, self.local_path) + playbook = os.path.relpath(playbook, project_path) # Filter files in a roles subdirectory. if 'roles' in playbook.split(os.sep): continue # Filter files in a tasks subdirectory. if 'tasks' in playbook.split(os.sep): continue - playbooks.append(playbook) - return playbooks + results.append(playbook) + return results class Permission(CommonModelNameNotUnique): ''' diff --git a/lib/main/serializers.py b/lib/main/serializers.py index af5f2930b6..b1d1914a9e 100644 --- a/lib/main/serializers.py +++ b/lib/main/serializers.py @@ -163,20 +163,35 @@ class AuditTrailSerializer(BaseSerializer): class ProjectSerializer(BaseSerializer): - available_playbooks = serializers.Field(source='available_playbooks') + playbooks = serializers.Field(source='playbooks') + local_path_choices = serializers.SerializerMethodField('get_local_path_choices') class Meta: model = Project - fields = BASE_FIELDS + ('local_path', 'available_playbooks') + fields = BASE_FIELDS + ('local_path', 'local_path_choices') # 'default_playbook', 'scm_type') def get_related(self, obj): res = super(ProjectSerializer, self).get_related(obj) res.update(dict( organizations = reverse('main:projects_organizations_list', args=(obj.pk,)), + playbooks = reverse('main:projects_detail_playbooks', args=(obj.pk,)), )) return res + def get_local_path_choices(self, obj): + return Project.get_local_path_choices() + +class ProjectPlaybooksSerializer(ProjectSerializer): + + class Meta: + model = Project + fields = ('playbooks',) + + def to_native(self, obj): + ret = super(ProjectPlaybooksSerializer, self).to_native(obj) + return ret.get('playbooks', []) + class InventorySerializer(BaseSerializer): class Meta: diff --git a/lib/main/tasks.py b/lib/main/tasks.py index a2c5db1ac1..2ea3aeb2f5 100644 --- a/lib/main/tasks.py +++ b/lib/main/tasks.py @@ -201,7 +201,10 @@ class RunJob(Task): kwargs['ssh_key_path'] = self.build_ssh_key_path(job, **kwargs) kwargs['passwords'] = self.build_passwords(job, **kwargs) args = self.build_args(job, **kwargs) - cwd = job.project.local_path + cwd = job.project.get_project_path() + if not cwd: + raise RuntimeError('project local_path %s cannot be found' % + project.local_path) env = self.build_env(job, **kwargs) status, stdout = self.run_pexpect(job_pk, args, cwd, env, kwargs['passwords']) diff --git a/lib/main/tests/base.py b/lib/main/tests/base.py index 2c8fd094b1..a783a490f6 100644 --- a/lib/main/tests/base.py +++ b/lib/main/tests/base.py @@ -95,8 +95,8 @@ class BaseTestMixin(object): test_playbook_file.write(playbook_content) test_playbook_file.close() return Project.objects.create( - name=name, description=description, local_path=project_dir, - created_by=created_by, + name=name, description=description, + local_path=os.path.basename(project_dir), created_by=created_by, #scm_type='git', default_playbook='foo.yml', ) diff --git a/lib/main/tests/jobs.py b/lib/main/tests/jobs.py index 5844d03b4c..3f84459484 100644 --- a/lib/main/tests/jobs.py +++ b/lib/main/tests/jobs.py @@ -315,7 +315,7 @@ class BaseJobTest(BaseTest): job_type='check', inventory= self.inv_eng, project=self.proj_dev, - playbook=self.proj_dev.available_playbooks[0], + playbook=self.proj_dev.playbooks[0], created_by=self.user_sue, ) self.jt_eng_run = JobTemplate.objects.create( @@ -323,7 +323,7 @@ class BaseJobTest(BaseTest): job_type='run', inventory= self.inv_eng, project=self.proj_dev, - playbook=self.proj_dev.available_playbooks[0], + playbook=self.proj_dev.playbooks[0], created_by=self.user_sue, ) @@ -334,7 +334,7 @@ class BaseJobTest(BaseTest): job_type='check', inventory= self.inv_sup, project=self.proj_test, - playbook=self.proj_test.available_playbooks[0], + playbook=self.proj_test.playbooks[0], created_by=self.user_sue, ) self.jt_sup_run = JobTemplate.objects.create( @@ -342,7 +342,7 @@ class BaseJobTest(BaseTest): job_type='run', inventory= self.inv_sup, project=self.proj_test, - playbook=self.proj_test.available_playbooks[0], + playbook=self.proj_test.playbooks[0], created_by=self.user_sue, ) @@ -353,7 +353,7 @@ class BaseJobTest(BaseTest): job_type='check', inventory= self.inv_ops_east, project=self.proj_prod, - playbook=self.proj_prod.available_playbooks[0], + playbook=self.proj_prod.playbooks[0], credential=self.cred_ops_east, created_by=self.user_sue, ) @@ -362,7 +362,7 @@ class BaseJobTest(BaseTest): job_type='run', inventory= self.inv_ops_east, project=self.proj_prod, - playbook=self.proj_prod.available_playbooks[0], + playbook=self.proj_prod.playbooks[0], credential=self.cred_ops_east, created_by=self.user_sue, ) @@ -371,7 +371,7 @@ class BaseJobTest(BaseTest): job_type='check', inventory= self.inv_ops_west, project=self.proj_prod, - playbook=self.proj_prod.available_playbooks[0], + playbook=self.proj_prod.playbooks[0], credential=self.cred_ops_west, created_by=self.user_sue, ) @@ -380,7 +380,7 @@ class BaseJobTest(BaseTest): job_type='run', inventory= self.inv_ops_west, project=self.proj_prod, - playbook=self.proj_prod.available_playbooks[0], + playbook=self.proj_prod.playbooks[0], credential=self.cred_ops_west, created_by=self.user_sue, ) @@ -454,7 +454,7 @@ class JobTemplateTest(BaseJobTest): inventory = self.inventory.pk, project = self.project.pk, job_type = PERM_INVENTORY_DEPLOY, - playbook = self.project.available_playbooks[0], + playbook = self.project.playbooks[0], ) with self.current_user(self.normal_django_user): response = self.post(url, data, expect=201) diff --git a/lib/main/tests/projects.py b/lib/main/tests/projects.py index b5c3fe96ac..5ee9eeb394 100644 --- a/lib/main/tests/projects.py +++ b/lib/main/tests/projects.py @@ -42,7 +42,7 @@ class ProjectsTest(BaseTest): self.setup_users() self.organizations = self.make_organizations(self.super_django_user, 10) - self.projects = self.make_projects(self.normal_django_user, 10) + self.projects = self.make_projects(self.normal_django_user, 10, TEST_PLAYBOOK) # add projects to organizations in a more or less arbitrary way for project in self.projects[0:2]: @@ -100,50 +100,56 @@ class ProjectsTest(BaseTest): # here is a user without any permissions... return ('nobody', 'nobody') - def test_available_playbooks(self): + def test_playbooks(self): def write_test_file(project, name, content): - full_path = os.path.join(project.local_path, name) + full_path = os.path.join(project.get_project_path(), name) if not os.path.exists(os.path.dirname(full_path)): os.makedirs(os.path.dirname(full_path)) - f = file(os.path.join(project.local_path, name), 'wb') + f = file(full_path, 'wb') f.write(content) f.close() # Invalid local_path project = self.projects[0] - project.local_path = os.path.join(project.local_path, - 'does_not_exist') + project.local_path = 'path_does_not_exist' project.save() - self.assertEqual(len(project.available_playbooks), 0) + self.assertFalse(project.get_project_path()) + self.assertEqual(len(project.playbooks), 0) # Simple playbook project = self.projects[1] + self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'foo.yml', TEST_PLAYBOOK) - self.assertEqual(len(project.available_playbooks), 1) + self.assertEqual(len(project.playbooks), 2) # Other files project = self.projects[2] + self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'foo.txt', 'not a playbook') - self.assertEqual(len(project.available_playbooks), 0) + self.assertEqual(len(project.playbooks), 1) # Empty playbook project = self.projects[3] + self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'blah.yml', '') - self.assertEqual(len(project.available_playbooks), 0) + self.assertEqual(len(project.playbooks), 1) # Invalid YAML project = self.projects[4] + self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'blah.yml', TEST_PLAYBOOK + '----') - self.assertEqual(len(project.available_playbooks), 0) + self.assertEqual(len(project.playbooks), 1) # No hosts or includes project = self.projects[5] + self.assertEqual(len(project.playbooks), 1) playbook_content = TEST_PLAYBOOK.replace('hosts', 'hoists') write_test_file(project, 'blah.yml', playbook_content) - self.assertEqual(len(project.available_playbooks), 0) + self.assertEqual(len(project.playbooks), 1) # Playbook in roles folder project = self.projects[6] + self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'roles/blah.yml', TEST_PLAYBOOK) - self.assertEqual(len(project.available_playbooks), 0) + self.assertEqual(len(project.playbooks), 1) # Playbook in tasks folder project = self.projects[7] + self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'tasks/blah.yml', TEST_PLAYBOOK) - self.assertEqual(len(project.available_playbooks), 0) - + self.assertEqual(len(project.playbooks), 1) def test_mainline(self): @@ -180,6 +186,11 @@ class ProjectsTest(BaseTest): self.delete(project, expect=204, auth=self.get_normal_credentials()) self.get(project, expect=404, auth=self.get_normal_credentials()) + # can list playbooks for projects + proj_playbooks = '/api/v1/projects/%d/playbooks/' % self.projects[2].pk + got = self.get(proj_playbooks, expect=200, auth=self.get_super_credentials()) + self.assertEqual(got, self.projects[2].playbooks) + # can list member organizations for projects proj_orgs = '/api/v1/projects/1/organizations/' # only usable as superuser diff --git a/lib/main/tests/tasks.py b/lib/main/tests/tasks.py index 37b092f8db..fe74215354 100644 --- a/lib/main/tests/tasks.py +++ b/lib/main/tests/tasks.py @@ -177,7 +177,7 @@ class RunJobTest(BaseCeleryTest): 'credential': self.credential, } try: - opts['playbook'] = self.project.available_playbooks[0] + opts['playbook'] = self.project.playbooks[0] except (AttributeError, IndexError): pass opts.update(kwargs) @@ -196,7 +196,7 @@ class RunJobTest(BaseCeleryTest): 'credential': self.credential, } try: - opts['playbook'] = self.project.available_playbooks[0] + opts['playbook'] = self.project.playbooks[0] except (AttributeError, IndexError): pass opts.update(kwargs) diff --git a/lib/main/urls.py b/lib/main/urls.py index af6e72f77b..0008d979f7 100644 --- a/lib/main/urls.py +++ b/lib/main/urls.py @@ -50,6 +50,7 @@ users_urls = patterns('lib.main.views', projects_urls = patterns('lib.main.views', url(r'^$', 'projects_list'), url(r'^(?P[0-9]+)/$', 'projects_detail'), + url(r'^(?P[0-9]+)/playbooks/$', 'projects_detail_playbooks'), url(r'^(?P[0-9]+)/organizations/$', 'projects_organizations_list'), ) diff --git a/lib/main/views.py b/lib/main/views.py index bab9c4c9f2..c56467da80 100644 --- a/lib/main/views.py +++ b/lib/main/views.py @@ -403,6 +403,12 @@ class ProjectsDetail(BaseDetail): serializer_class = ProjectSerializer permission_classes = (CustomRbac,) +class ProjectsDetailPlaybooks(generics.RetrieveAPIView): + + model = Project + serializer_class = ProjectPlaybooksSerializer + permission_classes = (CustomRbac,) + class ProjectsOrganizationsList(BaseSubList): model = Organization