From 77695bd9598e20cd76ea1dd93bb47b7f7f758110 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Mon, 25 Mar 2013 16:41:21 -0400 Subject: [PATCH] Working on surfacing inventory objects. --- TODO.md | 2 +- lib/main/migrations/0008_changes.py | 252 ++++++++++++++++++++++++++++ lib/main/models/__init__.py | 34 +++- lib/main/serializers.py | 16 ++ lib/main/tests/__init__.py | 2 +- lib/main/tests/inventory.py | 81 ++++++--- lib/main/tests/organizations.py | 1 + lib/main/views.py | 26 +++ lib/urls.py | 24 +-- 9 files changed, 401 insertions(+), 37 deletions(-) create mode 100644 lib/main/migrations/0008_changes.py diff --git a/TODO.md b/TODO.md index 19084743bd..971aaade6e 100644 --- a/TODO.md +++ b/TODO.md @@ -28,4 +28,4 @@ TWEAKS/ASSORTED * allow multiple playbook execution types per project, different --tag choices, different --limit choices (maybe just free form in the job for now?) * permissions infrastructure about who can kick off what kind of jobs * it would be nice if POSTs to subcollections used the permissions of the regular collection POST rules and then called the PUT code. - +* root API discovery resource at /api and /api/v1 diff --git a/lib/main/migrations/0008_changes.py b/lib/main/migrations/0008_changes.py new file mode 100644 index 0000000000..4c9bd6c1b6 --- /dev/null +++ b/lib/main/migrations/0008_changes.py @@ -0,0 +1,252 @@ +# -*- 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): + # Deleting field 'Permission.job_type' + db.delete_column(u'main_permission', 'job_type') + + # Adding field 'Permission.inventory' + db.add_column(u'main_permission', 'inventory', + self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='permissions', null=True, on_delete=models.SET_NULL, to=orm['main.Inventory']), + keep_default=False) + + # Adding field 'Permission.permission_type' + db.add_column(u'main_permission', 'permission_type', + self.gf('django.db.models.fields.CharField')(default='run', max_length=64), + keep_default=False) + + + def backwards(self, orm): + # Adding field 'Permission.job_type' + db.add_column(u'main_permission', 'job_type', + self.gf('django.db.models.fields.CharField')(default='run', max_length=64), + keep_default=False) + + # Deleting field 'Permission.inventory' + db.delete_column(u'main_permission', 'inventory_id') + + # Deleting field 'Permission.permission_type' + db.delete_column(u'main_permission', 'permission_type') + + + 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_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']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['main.Project']", 'blank': 'True', 'null': 'True'}), + 'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'ssh_key_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '4096', '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'}), + 'sudo_password': ('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': {'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_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']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', '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']"}) + }, + 'main.host': { + 'Meta': {'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_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']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'host_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}) + }, + 'main.inventory': { + 'Meta': {'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_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']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'", 'null': 'True', 'on_delete': 'models.SET_NULL', '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.launchjob': { + 'Meta': {'object_name': 'LaunchJob'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'launchjob_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'launchjob\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'launch_jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + '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': "'launch_jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Inventory']", 'blank': 'True', 'null': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'launch_jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['main.Project']", 'blank': 'True', 'null': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'launchjob_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'launch_jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['auth.User']", 'blank': 'True', 'null': 'True'}) + }, + 'main.launchjobstatus': { + 'Meta': {'object_name': 'LaunchJobStatus'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'launchjobstatus_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'launchjobstatus\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'launch_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'launch_job_statuses'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.LaunchJob']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'result_data': ('django.db.models.fields.TextField', [], {}), + 'status': ('django.db.models.fields.IntegerField', [], {}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'launchjobstatus_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}) + }, + '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_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']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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_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']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', '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_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']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'default_playbook': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventories': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'projects'", 'blank': 'True', 'to': "orm['main.Inventory']"}), + 'local_repository': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + '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_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']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'}), + 'organizations': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'teams'", 'symmetrical': 'False', '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_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']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'data': ('django.db.models.fields.TextField', [], {}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'variable_data'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Group']"}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'variable_data'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', '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 1e7cb05f7b..13a9268f00 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -26,9 +26,30 @@ import exceptions # TODO: jobs and events model TBD # TODO: reporting model TBD +PERM_INVENTORY_READ = 'read' +PERM_INVENTORY_WRITE = 'write' +PERM_INVENTORY_DEPLOY = 'run' +PERM_INVENTORY_CHECK = 'check' + JOB_TYPE_CHOICES = [ - ('run', _('Run')), - ('check', _('Check')), + (PERM_INVENTORY_DEPLOY, _('Run')), + (PERM_INVENTORY_CHECK, _('Check')), +] + +PERMISSION_TYPES = [ + PERM_INVENTORY_READ, + PERM_INVENTORY_WRITE, + PERM_INVENTORY_DEPLOY, + PERM_INVENTORY_CHECK, +] + +PERMISSION_TYPES_ALLOWING_INVENTORY_READ = PERMISSION_TYPES + +PERMISSION_TYPE_CHOICES = [ + (PERM_INVENTORY_READ, _('Read Inventory')), + (PERM_INVENTORY_WRITE, _('Write Inventory')), + (PERM_INVENTORY_DEPLOY, _('Deploy To Inventory')), + (PERM_INVENTORY_CHECK, _('Deploy To Inventory (Dry Run)')), ] class EditHelper(object): @@ -231,6 +252,10 @@ class Inventory(CommonModel): verbose_name_plural = _('inventories') organization = models.ForeignKey(Organization, null=True, on_delete=SET_NULL, related_name='inventories') + + def get_absolute_url(self): + import lib.urls + return reverse(lib.urls.views_InventoryDetail, args=(self.pk,)) def __unicode__(self): if self.organization: @@ -364,7 +389,8 @@ class Permission(CommonModel): user = models.ForeignKey('auth.User', null=True, on_delete=SET_NULL, blank=True, related_name='permissions') project = models.ForeignKey('Project', null=True, on_delete=SET_NULL, blank=True, related_name='permissions') team = models.ForeignKey('Team', null=True, on_delete=SET_NULL, blank=True, related_name='permissions') - job_type = models.CharField(max_length=64, choices=JOB_TYPE_CHOICES) + inventory = models.ForeignKey('Inventory', null=True, on_delete=SET_NULL, blank=True, related_name='permissions') + permission_type = models.CharField(max_length=64, choices=PERMISSION_TYPE_CHOICES) # TODO: other job types (later) @@ -380,6 +406,8 @@ class LaunchJob(CommonModel): credential = models.ForeignKey('Credential', on_delete=SET_NULL, null=True, default=None, blank=True, related_name='launch_jobs') project = models.ForeignKey('Project', on_delete=SET_NULL, null=True, default=None, blank=True, related_name='launch_jobs') user = models.ForeignKey('auth.User', on_delete=SET_NULL, null=True, default=None, blank=True, related_name='launch_jobs') + + # JOB_TYPE_CHOICES are a subset of PERMISSION_TYPE_CHOICES job_type = models.CharField(max_length=64, choices=JOB_TYPE_CHOICES) def start(self): diff --git a/lib/main/serializers.py b/lib/main/serializers.py index c92d8db93a..186596755a 100644 --- a/lib/main/serializers.py +++ b/lib/main/serializers.py @@ -78,6 +78,22 @@ class ProjectSerializer(BaseSerializer): # FIXME: add related resources: inventories return dict() + +class InventorySerializer(BaseSerializer): + + # add the URL and related resources + url = serializers.CharField(source='get_absolute_url', read_only=True) + related = serializers.SerializerMethodField('get_related') + + class Meta: + model = Inventory + fields = ('url', 'id', 'name', 'description', 'creation_date') + + def get_related(self, obj): + # FIXME: add related resources: inventories + return dict() + + class TeamSerializer(BaseSerializer): # add the URL and related resources diff --git a/lib/main/tests/__init__.py b/lib/main/tests/__init__.py index 05f7c2d9aa..d3ec092ade 100644 --- a/lib/main/tests/__init__.py +++ b/lib/main/tests/__init__.py @@ -1,4 +1,4 @@ from lib.main.tests.organizations import OrganizationsTest from lib.main.tests.users import UsersTest - +from lib.main.tests.inventory import InventoryTest diff --git a/lib/main/tests/inventory.py b/lib/main/tests/inventory.py index d2284df272..ddd085aa8f 100644 --- a/lib/main/tests/inventory.py +++ b/lib/main/tests/inventory.py @@ -17,17 +17,65 @@ from lib.main.tests.base import BaseTest class InventoryTest(BaseTest): def setUp(self): - super(UsersTest, self).setUp() + super(InventoryTest, self).setUp() self.setup_users() self.organizations = self.make_organizations(self.super_django_user, 1) self.organizations[0].admins.add(self.normal_django_user) self.organizations[0].users.add(self.other_django_user) self.organizations[0].users.add(self.normal_django_user) + + self.inventory_a = Inventory.objects.create(name='inventory-a', description='foo', organization=self.organizations[0]) + self.inventory_b = Inventory.objects.create(name='inventory-b', description='foo', organization=self.organizations[0]) + # the normal user is an org admin of org 0 + + # create a permission here on the 'other' user so they have edit access on the org + # TODO + + # and make one more user that won't be a part of any org, just for negative-access testing + + self.nobody_django_user = User.objects.create(username='nobody', password='nobody') + + def get_nobody_credentials(self): + # here is a user without any permissions... + return ('nobody', 'nobody') + def test_main_line(self): - - #url = '/api/v1/users/' - #new_user = dict(username='blippy') + + # some basic URLs... + inventories = '/api/v1/inventories/' + inventories_1 = '/api/v1/inventories/1/' + inventories_2 = '/api/v1/inventories/2/' + hosts = '/api/v1/hosts/' + groups = '/api/v1/groups/' + variables = '/api/v1/variables/' + + # a super user can list inventories + data = self.get(inventories, expect=200, auth=self.get_super_credentials()) + print data + self.assertEquals(data['count'], 2) + + # an org admin can list inventories but is filtered to what he adminsters + data = self.get(inventories, expect=200, auth=self.get_normal_credentials()) + self.assertEquals(data['count'], 1) + + # a user who is on a team who has a read permissions on an inventory can see filtered inventories + data = self.get(inventories, expect=200, auth=self.get_other_credentials()) + self.assertEquals(data['count'], 0) + + # a regular user not part of anything cannot see any inventories + data = self.get(inventories, expect=200, auth=self.get_nobody_credentials()) + self.assertEquals(data['count'], 0) + + # a super user can get inventory records + data = self.get(detail_url_1, expect=200, auth=self.get_super_credentials()) + self.assertEquals(data['name'], 'inventory-a') + + # a user who is on a team who has read permissions on an inventory can see inventory records + + # a regular user cannot read any inventory records + + #new_user2 = dict(username='blippy2') # a super user can create inventory @@ -103,22 +151,6 @@ class InventoryTest(BaseTest): # a normal user with inventory edit permissions can associate subgroups - # a super user can list inventories - - # an org admin can list inventories - - # a user who is on a team who has a read permissions on an inventory can see filtered inventories - - # a regular user not part of anything cannot see any inventories - - # a super user can get inventory records - - # an org admin can get inventory records - - # a user who is on a team who has read permissions on an inventory can see inventory records - - # a regular user cannot read any inventory records - # a super user can get a group record # an org admin can get a group record @@ -231,9 +263,16 @@ class InventoryTest(BaseTest): # variable records + # on an inventory resource, I can see related resources for hosts and groups and permissions + # and these work + # on a host resource, I can see related resources variables and inventories + # and these work + + # on a group resource, I can see related resources for variables, inventories, and children + # and these work - + diff --git a/lib/main/tests/organizations.py b/lib/main/tests/organizations.py index 586f469c8b..e8146c896d 100644 --- a/lib/main/tests/organizations.py +++ b/lib/main/tests/organizations.py @@ -352,4 +352,5 @@ class OrganizationsTest(BaseTest): # also check that DELETE on the collection doesn't work self.delete(self.collection(), expect=405, auth=self.get_super_credentials()) +# TODO: tests for tag disassociation diff --git a/lib/main/views.py b/lib/main/views.py index 698e33b8f4..2bcbda9ac2 100644 --- a/lib/main/views.py +++ b/lib/main/views.py @@ -253,4 +253,30 @@ class UsersDetail(BaseDetail): obj.set_password(request.DATA['password']) obj.save() request.DATA.pop('password') + +class InventoryList(BaseList): + + model = Inventory + serializer_class = InventorySerializer + permission_classes = (CustomRbac,) + + def _get_queryset(self): + ''' I can see inventory when I'm a superuser, an org admin of the inventory, or I have permissions on it ''' + base = Inventory.objects + if self.request.user.is_superuser: + return base.all() + admin_of = base.filter(organization__admins__in = [ self.request.user ]).distinct() + has_perms = base.filter( + permissions__user__in = [ self.request.user ], + permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + return admin_of | has_perms + +class InventoryDetail(BaseDetail): + + model = Inventory + serializer_class = InventorySerializer + permission_classes = (CustomRbac,) + + diff --git a/lib/urls.py b/lib/urls.py index 4e4e84b90c..261abb6d00 100644 --- a/lib/urls.py +++ b/lib/urls.py @@ -36,16 +36,16 @@ views_UsersTeamsList = views.UsersTeamsList.as_view() views_UsersOrganizationsList = views.UsersOrganizationsList.as_view() views_UsersAdminOrganizationsList = views.UsersAdminOrganizationsList.as_view() - - # projects service -views_ProjectsDetail = views.OrganizationsDetail.as_view() +views_ProjectsDetail = views.OrganizationsDetail.as_view() # audit trail service # team service # inventory service +views_InventoryList = views.InventoryList.as_view() +views_InventoryDetail = views.InventoryDetail.as_view() # group service @@ -65,13 +65,13 @@ views_TagsDetail = views.TagsDetail.as_view() urlpatterns = patterns('', # organizations service - url(r'^api/v1/organizations/$', views_OrganizationsList), - url(r'^api/v1/organizations/(?P[0-9]+)/$', views_OrganizationsDetail), - url(r'^api/v1/organizations/(?P[0-9]+)/audit_trail/$', views_OrganizationsAuditTrailList), - url(r'^api/v1/organizations/(?P[0-9]+)/users/$', views_OrganizationsUsersList), - url(r'^api/v1/organizations/(?P[0-9]+)/admins/$', views_OrganizationsAdminsList), - url(r'^api/v1/organizations/(?P[0-9]+)/projects/$', views_OrganizationsProjectsList), - url(r'^api/v1/organizations/(?P[0-9]+)/tags/$', views_OrganizationsTagsList), + url(r'^api/v1/organizations/$', views_OrganizationsList), + url(r'^api/v1/organizations/(?P[0-9]+)/$', views_OrganizationsDetail), + url(r'^api/v1/organizations/(?P[0-9]+)/audit_trail/$', views_OrganizationsAuditTrailList), + url(r'^api/v1/organizations/(?P[0-9]+)/users/$', views_OrganizationsUsersList), + url(r'^api/v1/organizations/(?P[0-9]+)/admins/$', views_OrganizationsAdminsList), + url(r'^api/v1/organizations/(?P[0-9]+)/projects/$', views_OrganizationsProjectsList), + url(r'^api/v1/organizations/(?P[0-9]+)/tags/$', views_OrganizationsTagsList), # users service url(r'^api/v1/users/$', views_UsersList), @@ -82,13 +82,15 @@ urlpatterns = patterns('', url(r'^api/v1/users/(?P[0-9]+)/admin_of_organizations/$', views_UsersAdminOrganizationsList), # projects service - url(r'^api/v1/projects/(?P[0-9]+)/$', views_ProjectsDetail), + url(r'^api/v1/projects/(?P[0-9]+)/$', views_ProjectsDetail), # audit trail service # team service # inventory service + url(r'^api/v1/inventories/$', views_InventoryList), + url(r'^api/v1/inventories/(?P[0-9]+)/$', views_InventoryDetail), # group service